Skip to content

Last updated:

Development Process

This guide covers the day-to-day development workflow for the CPR (Computerized Patient Record) frontend, built with Nuxt 4, Vue 3, TypeScript, and Tailwind CSS.

Prerequisites

  • Node.js -- version 18.x or later (LTS recommended)
  • npm -- ships with Node.js
  • A running instance of the CPR backend API (default: http://localhost:8000/api/v1)

Clone and Setup

bash
git clone https://github.com/RackApp-IT-Solutions/cpr-frontend.git
cd cpr-frontend
npm install

npm install triggers the postinstall script, which runs nuxt prepare to generate TypeScript types and the .nuxt directory.

Environment Variables

Copy the example env file and fill in the values:

bash
cp .env.example .env
VariableDescriptionDefault
API_URLBackend API base URLhttp://localhost:8000/api/v1
SENTRY_DSNSentry DSN for error tracking(empty -- disables Sentry)

These are exposed via useRuntimeConfig().public in the app. You do not need SENTRY_DSN for local development.

Running the Dev Server

bash
npm run dev

This starts the Nuxt development server with hot module replacement. The app runs in SPA mode (ssr: false). Nuxt DevTools are enabled automatically in non-production environments.

Project Structure (Nuxt 4)

Nuxt 4 uses app/ as the source directory. Key directories:

app/
  components/     # Auto-imported Vue components (no path prefix)
  composables/    # Auto-imported composables (useXxx pattern)
  layouts/        # Layout components
  middleware/     # Route middleware
  pages/          # File-based routing
  plugins/        # Nuxt plugins
  sevices/        # API service classes
  stores/         # Pinia stores
  types/          # TypeScript type definitions
  constants/      # Constant values and enums
  utils/          # Utility functions
  assets/css/     # Tailwind CSS entry point

Code Quality Checks

Always run these before committing:

bash
# Check for linting errors (ESLint + Prettier)
npm run lint

# Auto-fix linting errors
npm run lint:fix

# Check formatting
npm run format

# Auto-fix formatting
npm run format:fix

The project uses:

  • ESLint with the @nuxt/eslint config and the Prettier plugin to avoid rule conflicts
  • Prettier with single quotes, semicolons, 2-space tabs, trailing commas (es5), and 80-character print width

How to Add a New Feature

Follow this layered approach, working from the data layer up to the UI:

1. Define Types

Create app/types/{feature}.type.ts with the resource interface and any form data interface:

typescript
// app/types/insurance.type.ts
export interface InsuranceResource {
  id: number;
  provider_name: string;
  plan_type: string;
  coverage: number;
  status: 'active' | 'inactive';
  created_at?: string | null;
  updated_at?: string | null;
}

export interface InsuranceFormData {
  provider_name: string;
  plan_type: string;
  coverage: number;
  status: 'active' | 'inactive';
}

2. Create the Service

Create app/sevices/{feature}.service.ts. Services are classes that call the API via useApiFetch and handle error reporting to Sentry:

typescript
// app/sevices/insurance.service.ts
import type { ApiResponse, PaginatedResourceResponse } from '~/types/apiReponse.type';
import type { InsuranceResource, InsuranceFormData } from '~/types/insurance.type';
import * as Sentry from '@sentry/nuxt';

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

  async getInsurances(search?: string, filters?: { plan_type?: string | number }, page?: number) {
    try {
      const params: Record<string, string> = {};
      if (search) params.search = search;
      if (page) params.page = String(page);
      return await useApiFetch<PaginatedResourceResponse<InsuranceResource>>('/insurances/', {
        method: 'GET',
        params,
      });
    } catch (err: unknown) {
      Sentry.captureException(err);
      throw new Error(this.getErrorMessage(err));
    }
  }

  async createInsurance(params: InsuranceFormData): Promise<InsuranceResource> {
    // ...
  }

  async updateInsurance(id: number, params: InsuranceFormData): Promise<InsuranceResource> {
    // ...
  }

  async deleteInsurance(id: number): Promise<void> {
    // ...
  }
}

export const insuranceService = new InsuranceService();

Key conventions:

  • Use useApiFetch (from app/composables/useApiFetch.ts) for authenticated requests -- it injects the bearer token automatically.
  • Always wrap API calls in try/catch, call Sentry.captureException(err), and throw a user-friendly error message.
  • Export a singleton instance at the bottom of the file.

3. Create Composables

Create a directory app/composables/{feature}/ with one composable per operation:

FilePurpose
useInsurance.tsFetch list, populate store
useCreateInsurance.tsCreate operation with loading/error/success state
useUpdateInsurance.tsUpdate operation
useDeleteInsurance.tsDelete operation

Each composable follows the same pattern -- expose loading, error, success refs and an async action function:

typescript
export const useCreateInsurance = () => {
  const loading = ref(false);
  const error = ref('');
  const success = ref(false);

  const createInsurance = async (data: InsuranceFormData) => {
    loading.value = true;
    error.value = '';
    success.value = false;
    try {
      await insuranceService.createInsurance(data);
      success.value = true;
    } catch (err: unknown) {
      error.value = (err as { message?: string }).message || 'Failed to create insurance';
    } finally {
      loading.value = false;
    }
  };

  return { loading, error, success, createInsurance };
};

4. Create the Store

Create app/stores/{feature}.store.ts using Pinia's Composition API syntax:

typescript
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { InsuranceResource } from '~/types/insurance.type';
import type { APIPagination } from '~/types/api.type';

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

  return { list, pagination };
});

5. Build Components

Create components under app/components/{feature}/:

  • Table component -- renders the list from the store
  • Form components -- create and update forms
  • Filter component -- search and filter controls

Components are auto-imported (no manual import statements needed). The pathPrefix: false config means you reference them by filename only, e.g., <InsuranceForm />.

6. Create the Page

Add a page under app/pages/ following Nuxt file-based routing conventions.

How to Add a New CRUD Module

This is a step-by-step checklist for adding a complete CRUD module (using "supplier" as an example):

  1. Types -- Create app/types/supplier.type.ts with SupplierResource and SupplierFormData interfaces.

  2. Service -- Create app/sevices/supplier.service.ts with methods:

    • getSuppliers(search?, filters?, page?) -- GET with pagination
    • createSupplier(data) -- POST
    • updateSupplier(id, data) -- PUT
    • deleteSupplier(id) -- DELETE
  3. Store -- Create app/stores/supplier.store.ts with list and pagination refs.

  4. Composables -- Create app/composables/supplier/ directory with:

    • useSupplier.ts -- fetches list, writes to store
    • useCreateSupplier.ts
    • useUpdateSupplier.ts
    • useDeleteSupplier.ts
  5. Components -- Create app/components/configuration/supplier/ (or appropriate parent) with:

    • SupplierTable.vue -- data table with pagination
    • SupplierTableRow.vue -- individual row with edit/delete actions
    • SupplierFilters.vue -- search input + filter dropdowns
    • app/components/configuration/forms/SupplierForms/SupplierForm.vue -- create form
    • app/components/configuration/forms/SupplierForms/UpdateSupplierForm.vue -- edit form
  6. Page -- Add app/pages/configuration/supplier.vue (or a subdirectory if needed).

  7. Constants (optional) -- If the module has enum-like values (status options, category lists), add app/constants/supplier.ts.

  8. Verify -- Run npm run lint and npm run format before committing.

CPR - Clinical Patient Records