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
{
"success": true,
"data": {
"id": 1,
"name": "Fluffy"
},
"message": "Pet retrieved successfully"
}success(boolean): Alwaystrue.data(object|array): The primary payload of the response.message(string): A short, human-readable description of the result.
Error Response
{
"success": false,
"message": "The given data was invalid.",
"errors": {
"name": ["The name field is required."]
}
}success(boolean): Alwaysfalse.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, $message, $code): Standard success response.sendError($message, $code, $errors): Standard error response.sendSuccessWithMeta($message, $data, $code): 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:
#[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:
- Update backend OpenAPI annotations.
- Run
bun run api:generatein thefrontenddirectory. - Import the generated hooks from
@/api/generated/.
Example:
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:
- Axios Interceptor: Defined in
frontend/src/api/axios.ts, it automatically unwraps thedatakey from the backend's JSON envelope. - 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:
{
"id": 1,
"name": "Mèo Siamese",
"slug": "siamese"
}CI/Deployment Integration
The API client is regenerated automatically during deployment:
- Docker Build: The Dockerfile runs
bun run api:generatebefore building the frontend assets. - Deploy Script:
utils/deploy.shincludes a pre-build check that runsbun run api:generateto catch OpenAPI spec drift early. - Local Check: Run
bun run api:checkto 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 separateauthApiAxios instance. - CSRF Token (
/sanctum/csrf-cookie): Handled directly via thecsrf()helper function.
These exceptions are intentional and ensure the generated client focuses on the /api/* routes documented in the OpenAPI spec.