Skip to content

API Conventions ​

This document outlines the conventions used for API development in Meo Mai Moi, ensuring consistency across the backend, documentation, and frontend.

Response Envelope ​

All API responses follow a standard JSON envelope format. This consistency allows the frontend to have a single entry point for handling data, messages, and errors.

Success Response ​

json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Fluffy"
  },
  "message": "Pet retrieved successfully"
}
  • success (boolean): Always true.
  • data (object|array): The primary payload of the response.
  • message (string): A short, human-readable description of the result.

Exception:

  • 204 No Content responses intentionally return an empty body rather than the JSON envelope. Use these for idempotent delete/mark-read style endpoints where there is no payload to return.

Error Response ​

json
{
  "success": false,
  "message": "The given data was invalid.",
  "errors": {
    "name": ["The name field is required."]
  }
}
  • success (boolean): Always false.
  • message (string): A human-readable description of the error.
  • errors (object): Optional. Field-specific validation errors.

Backend Implementation ​

ApiResponseTrait ​

Controllers should use the App\Traits\ApiResponseTrait to generate responses.

  • sendSuccess($data, $statusCode): Standard success response.
  • sendError($message, $statusCode): Standard error response.
  • sendNoContent(): Empty 204 No Content response.
  • sendSuccessWithMeta($data, $message, $statusCode): Success response where the message is also nested in data (useful for frontend interceptors).

OpenAPI (Swagger) Documentation ​

All endpoints must be annotated with OpenAPI attributes. Use the centralized schemas in app/Schemas/ResponseSchemas.php to define the response structure:

php
#[OA\Response(
    response: 200,
    description: 'Success',
    content: new OA\JsonContent(ref: '#/components/schemas/PetResponse')
)]

Frontend Consumption ​

Typesafe API Client (Orval) ​

We use Orval to generate a fully typesafe API client and React Query hooks from our backend's OpenAPI api-docs.json. This eliminates manual wiring of query keys, response types, and invalidations.

Workflow:

  1. Update backend OpenAPI annotations.
  2. Run vp run api:generate in the frontend directory.
  3. Import the generated hooks from @/api/generated/.

Example:

typescript
import { useGetPets } from '@/api/generated/pets/pets'

const { data: pets } = useGetPets() // pets is automatically typed as Pet[]

The generated client is configured via frontend/src/api/orval-mutator.ts. It uses our centralized Axios instance and automatically accounts for the data envelope unwrapping at both the runtime (via interceptors) and type level (via Orval transformers).

Automatic Unwrapping ​

We use two mechanisms to simplify data access:

  1. Axios Interceptor: Defined in frontend/src/api/axios.ts, it automatically unwraps the data key from the backend's JSON envelope.
  2. Orval Transformer: Defined in frontend/orval.config.ts, it unwraps the { data: T } type in the generated hooks, so callers receive the payload directly.

Both work together to ensure that const { data } = useGetPets() gives you the actual list of pets, not the { data: pets } structure.

Internationalization (i18n) ​

For models with translatable fields (like PetType, Category, and City), the backend uses a concern that automatically serializes these fields as strings based on the current app locale (determined by the Accept-Language header).

  • Backend: Data is stored as JSONB in the database.
  • API Response: Returns a simple string for the current locale.
  • Frontend: Receives a string, no special handling required for display.

Example Category response for Accept-Language: vi:

json
{
  "id": 1,
  "name": "Mèo Siamese",
  "slug": "siamese"
}

Media Images ​

Spatie Media Library-backed images should use the shared MediaImage payload where practical. Gallery-style fields such as pet photos, helper profile photos, and medical record photos return arrays of image objects. Single-image fields can keep their legacy URL field for compatibility while also exposing a structured object, such as avatar_url plus avatar or photo_url plus photo.

json
{
  "id": 123,
  "url": "https://example.test/storage/...",
  "thumb_url": "https://example.test/storage/...",
  "medium_url": "https://example.test/storage/...",
  "webp_url": "https://example.test/storage/...",
  "srcset": "https://example.test/storage/... 640w, https://example.test/storage/... 1280w",
  "sources": [
    {
      "type": "image/webp",
      "srcset": "https://example.test/storage/... 640w, https://example.test/storage/... 1280w"
    }
  ],
  "width": 1280,
  "height": 960,
  "is_primary": true,
  "processing": false
}
  • url is the default display URL. It may point to a generated conversion rather than the original upload.
  • thumb_url, medium_url, and webp_url are nullable conversion URLs.
  • srcset contains fallback-format responsive candidates and sources contains alternative-format candidates suitable for <picture>.
  • width and height are nullable for legacy or unsupported media; new raster uploads record reliable intrinsic dimensions when the file enters Media Library.
  • is_primary marks the ordered primary image for galleries; single-image payloads set it to true.
  • processing is true when one or more expected generated conversions are not ready yet.
  • Rendering contexts choose their own HTML sizes value because layout knowledge belongs to the frontend, not the API.
  • After changing media response shapes, update OpenAPI annotations and run vp run api:generate plus vp run api:check.
  • After deploying responsive media support to an installation with existing uploads, run php artisan media:optimize-images --force once. The command records missing dimensions and queues current responsive/WebP derivatives without changing originals.

CI/Deployment Integration ​

The API client is regenerated automatically during deployment:

  1. Docker Build: The Dockerfile runs the frontend build path, which includes vp run api:generate before building the frontend assets.
  2. Deploy Script: utils/deploy.sh includes a pre-build check that runs vp run api:generate to catch OpenAPI spec drift early.
  3. Local Check: Run vp run api:check to verify generated code matches the committed OpenAPI spec.

Exceptions (Manual API Calls) ​

Some endpoints remain outside the generated client by design:

  • Fortify Auth Routes (/login, /register, /logout, /forgot-password, /reset-password): These Laravel Fortify routes are not documented in OpenAPI and use a separate authApi Axios instance.
  • CSRF Token (/sanctum/csrf-cookie): Handled directly via the csrf() helper function.
  • Email Unsubscribe (GET /unsubscribe, POST /api/unsubscribe): Email links point at GET /unsubscribe with signed user, type, and 64-character hex token query parameters. Laravel redirects to the SPA settings page (/settings/notifications?unsubscribe=1&...), where an authenticated user confirms via POST /api/unsubscribe. The API call is exempted from CSRF protection so the flow can complete without bootstrapping a Sanctum session first. Optional body fields: channel (email, in_app, telegram; default email) and scope (all, type; default all). The PWA service worker denylists /unsubscribe so the redirect reaches Laravel instead of the SPA shell.

These exceptions are intentional and ensure the generated client focuses on the /api/* routes documented in the OpenAPI spec.