Learning with AI: Vue 3 Project Structure — What Goes Where (and Why)

While writing a Vue.js frontend for a client, I decided to explore what ChatGPT’s “understanding” was of Vue project architecture. Large language models are very good at aggregating and summarizing data from a diverse list of sources which makes it a very useful tool for learning; however, always make sure to fact-check responses and request its sources.

The Quick Map

Views (Pages)
Route-level screens.
Components
Reusable UI building blocks used inside views (and other components).
Composables
Reusable logic (not UI) packaged as functions.
Stores
Centralized app/state management (Pinia).
Services
I/O boundaries (HTTP, storage, SDKs).
Helpers/Utils
Tiny, pure functions—no state, no I/O.

Views (a.k.a. Pages)

  • What: Route targets (e.g., /users/:id).
  • When: One per URL; coordinates data fetching, state wiring, and layout composition.
  • Do: Compose child components, call composables and stores, kick off service calls (often via composables).
  • Don’t: Stuff business logic here; keep them thin.
src/views/
  UsersView.vue
  LearningListView.vue

Components

  • What: Reusable, presentational or small feature units (buttons, cards, modals, data-table widgets).
  • When: You need UI that can be dropped into multiple places or to split a view into cleaner chunks.
  • Do: Accept props, emit events, remain as dumb as possible.
  • Don’t: Call remote APIs directly (that’s for services/composables); don’t hold global state.
src/components/
  learning/
    LearningCard.vue
    LearningEditor.vue
  ui/
    AppButton.vue
    AppDialog.vue

Composables (Vue 3 useXxx)

  • What: Reusable logic functions (stateful or stateless) that return reactive values/methods.
  • When: Share behavior across components (form handling, pagination, infinite scroll, feature logic).
  • Do: Encapsulate feature logic and optionally call services; return reactive state and functions.
  • Don’t: Become dumping grounds for app-wide state (that’s stores).
// src/composables/useLearning.ts
import { ref } from 'vue'
import { learningService } from '@/services/learning.service'

export function useLearning(service = learningService) {
  const items = ref([] as Array<any>)
  const loading = ref(false)
  const error = ref<string | null>(null)

  const fetch = async () => {
    loading.value = true
    error.value = null
    try { items.value = await service.list() }
    catch (e: any) { error.value = e?.message ?? 'Failed to load' }
    finally { loading.value = false }
  }

  return { items, loading, error, fetch }
}

Rule of thumb:

  • If multiple components need the same logic ⇒ composable.
  • If multiple routes/features must share canonical state or cache ⇒ store.

Stores (Pinia)

  • What: Centralized, long-lived state with actions/getters (e.g., auth, user profile, cart, feature caches).
  • When: State must survive route changes, be shared widely, or be the single source of truth.
  • Do: Own domain state; expose actions that call services; persist if needed.
  • Don’t: Put transient, purely local UI state here (keep that inside components/composables).
// src/stores/learning.ts
import { defineStore } from 'pinia'
import { learningService } from '@/services/learning.service'

export const useLearningStore = defineStore('learning', {
  state: () => ({ items: [] as any[], loading: false, error: null as string | null }),
  actions: {
    async load() {
      this.loading = true
      this.error = null
      try { this.items = await learningService.list() }
      catch (e: any) { this.error = e?.message ?? 'Failed to load' }
      finally { this.loading = false }
    }
  }
})

Services (API/SDK layer)

  • What: Thin, testable wrappers around I/O: HTTP calls, localStorage, Graph API, etc.
  • When: Anything that crosses the app boundary.
  • Do: Return typed DTOs or map to domain models; handle endpoints, headers, and errors here.
  • Don’t: Keep UI concerns or global state; avoid importing Vue reactivity.
// src/services/http.ts
import axios from 'axios'

export const http = axios.create({
  baseURL: import.meta.env.VITE_BACKEND_URL,
  withCredentials: true,
})
// src/services/learning.service.ts
import { http } from './http'

export type Learning = { id: number; title: string }
export type NewLearning = { title: string }

export const learningService = {
  list: async () => (await http.get<Learning[]>('/v1/main/profile/learning/1/50')).data,
  create: async (payload: NewLearning) => (await http.post('/v1/main/profile/learning', payload)).data,
}

Helpers / Utils

  • What: Small, pure functions with no side effects (formatters, sorters, validators).
  • When: You need shared math/string/date logic, and it doesn’t need Vue or I/O.
  • Do: Keep them tiny and unit-testable.
  • Don’t: Reference Vue, window, or services here.
// src/utils/date.ts
export const formatUtc = (d: string | Date) => new Date(d).toLocaleString()

How to Choose (Cheat Sheet)

NeedPut it in
New URL/screenView
Reusable UI pieceComponent
Reusable behavior (reactive)Composable
Shared, canonical app/feature stateStore
External calls (HTTP/SDK/storage)Service
Pure function (no I/O)Helper/Util

Suggested Folder Layout

src/
  assets/           # images, fonts
  components/       # shared UI
  composables/      # useXxx composables (feature or generic)
  router/           # routes
  services/         # http clients & SDK wrappers
    http.ts         # axios/fetch instance
  stores/           # pinia stores
  utils/            # pure functions
  views/            # route pages
  types/            # global TS types/interfaces
  styles/           # global css/scss
  App.vue
  main.ts

Conventions (Quick Hits)

  • Naming: useXxx for composables, SomethingService for services, useXxxStore for stores.
  • Typing: Put request/response typings in types/ or near the feature.
  • Imports: Components → composables & stores; composables → services/utils; stores → services; services → nothing Vue‑specific.
  • Testing:
    • utils/services ⇒ unit tests
    • composables ⇒ logic tests (with Vue Test Utils)
    • components/views ⇒ component tests

Common Pitfalls

  • Putting API calls inside components → move to services, call via composables/stores.
  • Using stores for ephemeral UI state → keep local or in a composable.
  • Bloated views → split into components + composables.
  • “God” composables mixing I/O and global state → if state must be canonical, push it into a store.

Next, I asked it where its “opinion” on do’s and dont’s came from. It couldn’t point to a specific repo or codebase and explained it doesn’t have direct access to its training data or private software. The guidance appears to come from common patterns in documentation, community guides, and widely shared examples—not any single project. I also asked it to scan public repos and name known projects; that didn’t change much, and the answers stayed largely the same.

Short answer: it’s not “my” opinion so much as a mash‑up of widely accepted patterns from official docs and community practices (Vue, Pinia, separation of concerns, and front‑end testing norms).

  • Keep UI pieces presentational and reusable.
  • Make logic shareable and testable (composables).
  • Avoid coupling UI to APIs (services).
  • Give shared, canonical state a single home (stores).
  • Keep pure utilities tiny and side‑effect free (helpers).

Finally I asked it to give me direct links to the references it used to answer my prompt.

What are you thoughts on using AI to summarize data for learning purposes? Do you like the do’s and don’ts section it provided?

Share this blog post:

Related Posts