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.