Skip to content

Last updated:

File Structure Standards

CPR uses Nuxt 4 with the app/ source directory convention. All application code lives inside app/, while project configuration files (nuxt.config.ts, package.json, tsconfig.json) remain at the project root.

Directory Overview

cpr-frontend/
├── nuxt.config.ts              # Nuxt configuration (modules, runtime config, etc.)
├── package.json
├── tsconfig.json
└── app/
    ├── app.vue                 # Root Vue component
    ├── spa-loading-template.html  # Loading screen shown while SPA hydrates
    ├── assets/                 # Static assets (CSS, images, fonts)
    │   └── css/
    │       └── main.css        # Tailwind CSS entry point
    ├── components/             # All Vue components (auto-imported)
    │   ├── ui/                 # Generic, reusable UI primitives
    │   ├── auth/               # Auth-related components
    │   ├── configuration/      # Configuration module components
    │   ├── patient/            # Patient module components
    │   ├── pharmacy/           # Pharmacy module components
    │   ├── queue/              # Queue module components
    │   ├── report/             # Report components
    │   ├── surgery/            # Surgery module components
    │   ├── profile/            # Profile components
    │   └── utils/              # Utility/helper components
    ├── composables/            # Vue composables organized by domain
    │   ├── useApiFetch.ts      # Core API fetch wrapper
    │   ├── useApi.ts           # Generic CRUD composable
    │   ├── useListFilters.ts   # Paginated list filter logic
    │   ├── useSnackBar.ts      # Toast notification composable
    │   ├── useConfirmDialog.ts # Confirmation dialog composable
    │   ├── useDialog.ts        # Dynamic dialog/modal composable
    │   ├── useFormValidation.ts # Form validation composable
    │   ├── useValidationRules.ts # Reusable validation rule helpers
    │   ├── useCanAccess.ts     # Permission checking
    │   ├── useStatusColor.ts   # Status-to-color mapping
    │   ├── useReportPrint.ts   # Print report helper
    │   ├── useQueryFilters.ts  # URL query parameter helpers
    │   ├── useQueryParam.ts    # Single query parameter helper
    │   ├── doctor/             # Doctor domain composables
    │   │   ├── useDoctor.ts
    │   │   ├── useCreateDoctor.ts
    │   │   ├── useUpdateDoctor.ts
    │   │   └── useDeleteDoctor.ts
    │   ├── patient/            # Patient domain composables
    │   ├── insurance/          # Insurance domain composables
    │   └── ...                 # Other domain folders
    ├── constants/              # Static constant values
    │   ├── insurance.ts
    │   ├── medicine.ts
    │   ├── pharmacy.ts
    │   └── procedure.ts
    ├── data/                   # Static data files (e.g., address lookups)
    │   └── address/
    ├── layouts/                # Nuxt layout components
    │   ├── default.vue         # Main app layout (sidebar + header)
    │   ├── auth.vue            # Login/reset password layout
    │   ├── configuration.vue   # Configuration module layout
    │   ├── pharmacy.vue        # Pharmacy module layout
    │   ├── queue.vue           # Queue module layout
    │   └── print.vue           # Print-optimized layout
    ├── middleware/              # Route middleware
    │   ├── auth.ts             # Authentication guard
    │   └── reset-password.ts   # Reset password flow guard
    ├── pages/                  # File-based routing
    │   ├── index.vue           # Root redirect
    │   ├── login.vue           # Login page
    │   ├── profile.vue         # User profile page
    │   ├── settings.vue        # User settings page
    │   ├── dashboard/          # Dashboard module
    │   ├── patient/            # Patient module
    │   ├── queue/              # Queue module
    │   ├── pharmacy/           # Pharmacy module
    │   ├── configuration/      # Configuration module (doctors, procedures, etc.)
    │   ├── surgery-schedule/   # Surgery scheduling module
    │   └── reset-password/     # Password reset flow
    ├── plugins/                # Nuxt plugins
    │   ├── permissions.ts      # Permission directive ($can)
    │   └── unregister-sw.client.ts  # Service worker cleanup
    ├── sevices/                # API service classes (note: folder name is intentional)
    │   ├── basicCrud.service.ts     # Base CRUD service class
    │   ├── doctor.service.ts        # Doctor API service
    │   ├── auth.sevice.ts           # Auth API service
    │   ├── insurance.service.ts     # Insurance API service
    │   └── ...                      # Other domain services
    ├── stores/                 # Pinia stores
    │   ├── auth.store.ts       # Auth state (user, token)
    │   ├── doctor.store.ts     # Doctor list + pagination
    │   ├── patient.store.ts    # Patient state
    │   ├── enums.store.ts      # Shared enum data from API
    │   ├── settings.ts         # App settings
    │   └── ...                 # Other domain stores
    ├── types/                  # TypeScript type definitions
    │   ├── apiReponse.type.ts  # API response types (ApiResponse, PaginatedResourceResponse)
    │   ├── doctor.type.ts      # Doctor and DoctorFilters interfaces
    │   ├── patient.type.ts     # Patient types
    │   ├── user.type.ts        # User/auth types
    │   └── ...                 # Other domain types
    └── utils/                  # Utility functions (auto-imported)
        ├── apiRequesUtils.ts   # generateQueryStringFromParams
        ├── dateFormatter.ts    # Date formatting helpers
        └── moneyFormatter.ts   # Currency formatting helpers

File Naming Conventions

DirectoryConventionExample
components/ui/PascalCase, Ui prefixUiButton.vue, UiTable.vue
components/[module]/PascalCase, descriptiveDoctorTable.vue, DoctorFilters.vue
composables/camelCase, use prefixuseDoctor.ts, useListFilters.ts
sevices/camelCase, .service.ts suffixdoctor.service.ts, basicCrud.service.ts
stores/camelCase, .store.ts suffixdoctor.store.ts, auth.store.ts
types/camelCase, .type.ts suffixdoctor.type.ts, apiReponse.type.ts
utils/camelCasedateFormatter.ts, moneyFormatter.ts
constants/camelCaseinsurance.ts, medicine.ts
pages/kebab-case directories, index.vue per routeconfiguration/doctors/index.vue
layouts/kebab-casedefault.vue, configuration.vue
middleware/kebab-caseauth.ts, reset-password.ts

Directory Details

components/

Components are auto-imported with pathPrefix: false (configured in nuxt.config.ts). This means you use component names directly without directory prefixes:

vue
<!-- Use DoctorTable directly, NOT ConfigurationDoctorDoctorTable -->
<DoctorTable :items="doctors" :pagination="pagination" />

Components are organized into two tiers:

  • ui/ -- Generic, reusable primitives (UiButton, UiTable, UiModal, etc.)
  • [module]/ -- Domain-specific components organized by module

See Component Architecture for full details.

composables/

Composables are organized in two ways:

  1. Root-level shared composables -- Used across the entire app (useApiFetch.ts, useSnackBar.ts, useListFilters.ts)
  2. Domain folders -- Composables specific to a feature domain (doctor/useDoctor.ts, doctor/useCreateDoctor.ts)

Each domain folder typically contains:

  • use[Entity].ts -- Fetch/list data
  • useCreate[Entity].ts -- Create operation
  • useUpdate[Entity].ts -- Update operation
  • useDelete[Entity].ts -- Delete operation

sevices/

Note

The folder is named sevices/ (missing an 'r'). This is an intentional existing convention -- do not rename it.

Service classes encapsulate API calls. Most services either extend BasicCrudService or follow the same pattern with custom methods. Each service file exports a singleton instance:

typescript
export const doctorService = new DoctorService();
export default doctorService;

stores/

Pinia stores hold shared reactive state. Each store uses the Composition API style (defineStore with a setup function). Stores typically hold:

  • A list ref for collection data
  • A pagination ref for pagination metadata
  • Any computed getters or actions needed

types/

TypeScript interfaces and types for each domain. Always define:

  • The main entity interface (e.g., Doctor)
  • A filters interface if the entity has list filtering (e.g., DoctorFilters)
  • Any related sub-types (e.g., DoctorRole)

utils/

Utility functions are auto-imported by Nuxt. Keep utilities pure (no Vue reactivity, no side effects). Current utilities:

  • generateQueryStringFromParams -- Converts GetApiParams<T> into flat query params for API requests
  • dateFormatter -- Date display helpers
  • moneyFormatter -- Currency display helpers

constants/

Static values that do not change at runtime. Use for dropdown options, fixed configuration values, or lookup tables that are not fetched from the API.

data/

Large static data sets (e.g., Philippine address data). Use for datasets too large or too static to fetch from the API.

Adding a New Module: Step-by-Step

This walkthrough uses a hypothetical "Appointment" module as an example.

Step 1: Define types

Create app/types/appointment.type.ts:

typescript
export interface Appointment {
  id: number;
  patient_id: number;
  doctor_id: number;
  scheduled_at: string;
  status: string;
  notes: string | null;
  created_at: string;
  updated_at: string;
}

export interface AppointmentFilters {
  search?: string;
  status?: string;
  doctor_id?: number;
  page?: number;
  perPage?: number;
}

Step 2: Create the service

Create app/sevices/appointment.service.ts:

typescript
import type { ApiResponse, paginatedData, PaginatedResourceResponse } from '~/types/apiReponse.type';
import type { FetchError } from 'ofetch';
import * as Sentry from '@sentry/nuxt';
import type { Appointment, AppointmentFilters } from '~/types/appointment.type';

class AppointmentService {
  private getErrorMessage(err: unknown): string {
    const fetchError = err as FetchError<unknown>;
    return (
      (fetchError.data as { message?: string })?.message ??
      fetchError.message ??
      'Something went wrong'
    );
  }

  async getAppointments(filters: AppointmentFilters = {}): Promise<paginatedData<Appointment[]>> {
    try {
      const params: Record<string, string> = {};
      if (filters.search) params.search = filters.search;
      if (filters.status) params.status = filters.status;
      if (filters.page) params.page = String(filters.page);
      if (filters.perPage) params.per_page = String(filters.perPage);

      const res = await useApiFetch<PaginatedResourceResponse<Appointment>>('/appointments', {
        method: 'GET',
        params,
      });

      return { data: res.data, ...res.meta, links: res.links } as paginatedData<Appointment[]>;
    } catch (err: unknown) {
      const message = this.getErrorMessage(err);
      Sentry.captureException(err);
      throw new Error(message);
    }
  }

  async createAppointment(params: Partial<Appointment>): Promise<Appointment> {
    try {
      const res: ApiResponse<Appointment> = await useApiFetch('/appointments', {
        method: 'POST',
        body: params,
      });
      return res.data;
    } catch (err: unknown) {
      const message = this.getErrorMessage(err);
      Sentry.captureException(err);
      throw new Error(message);
    }
  }

  // ... updateAppointment, deleteAppointment follow same pattern
}

export const appointmentService = new AppointmentService();
export default appointmentService;

Step 3: Create the store

Create app/stores/appointment.store.ts:

typescript
import { defineStore } from 'pinia';
import type { APIPagination } from '~/types/apiReponse.type';
import type { Appointment } from '~/types/appointment.type';

export const useAppointmentStore = defineStore('appointment', () => {
  const list = ref<Appointment[]>([]);
  const pagination = ref<APIPagination>({
    per_page: 15,
    total: 0,
    from: 0,
    to: 0,
    current_page: 1,
  });

  return { list, pagination };
});

Step 4: Create composables

Create domain composables in app/composables/appointment/:

  • useAppointment.ts -- Fetch list, populate store
  • useCreateAppointment.ts -- Create operation
  • useUpdateAppointment.ts -- Update operation
  • useDeleteAppointment.ts -- Delete operation

Step 5: Create components

Create components in app/components/[module]/appointment/:

  • AppointmentFilters.vue -- Search/filter bar
  • AppointmentTable.vue -- Data table
  • AppointmentTableRow.vue -- Single table row

Create form components in app/components/[module]/forms/AppointmentForms/:

  • AppointmentForm.vue -- Create form
  • UpdateAppointmentForm.vue -- Update form

Step 6: Create the page

Create app/pages/[module]/appointments/index.vue:

vue
<script setup lang="ts">
definePageMeta({
  layout: 'configuration', // or whichever layout fits
  middleware: 'auth',
});
</script>

The page composes filters, table, and form components together. See Component Architecture for the full page composition pattern.

Key Configuration

The project's nuxt.config.ts sets up the auto-import behavior:

typescript
export default defineNuxtConfig({
  ssr: false, // SPA mode

  components: [
    {
      path: '~/components',
      pathPrefix: false, // No directory prefix in component names
    },
  ],

  future: {
    compatibilityVersion: 4, // Nuxt 4 with app/ directory
  },

  runtimeConfig: {
    public: {
      API_URL: process.env.API_URL || 'http://localhost:8000/api/v1',
    },
  },
});

CPR - Clinical Patient Records