Skip to content

API Documentation Guide

This guide explains how to add OpenAPI documentation to your routes using the built-in documentation utilities.

Overview

The PRS Backend uses Fastify Swagger and Fastify Swagger UI to generate OpenAPI documentation from route schemas. We've added utilities to make it easy to generate these schemas from Zod validation schemas.

Documentation Utilities

The following utilities are available in the utils module:

  • documentRoute: Generates documentation for a route
  • documentPublicRoute: Generates documentation for a public route (no authentication required)
  • documentProtectedRoute: Generates documentation for a protected route (requires authentication)
  • standardResponses: Generates standard response schemas for common operations
  • zodToOpenApi: Converts a Zod schema to an OpenAPI schema
  • generateRouteSchema: Generates an OpenAPI schema for a route

Basic Usage

Here's a basic example of how to add documentation to a route:

JavaScript
const { documentProtectedRoute } = fastify.diScope.resolve('utils');
const { z } = require('zod');

// Define your schemas
const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(3).max(100),
  email: z.string().email(),
});

const createUserSchema = z.object({
  name: z.string().min(3).max(100),
  email: z.string().email(),
  password: z.string().min(8),
});

// Define your route
fastify.route({
  method: 'POST',
  url: '/users',
  preHandler: fastify.authenticate,
  ...documentProtectedRoute({
    tag: 'Users',
    summary: 'Create a new user',
    description: 'Creates a new user with the provided data',
    body: createUserSchema,
    response: {
      201: z.object({
        success: z.boolean(),
        message: z.string(),
        data: userSchema,
      }),
      400: z.object({
        success: z.boolean(),
        message: z.string(),
        errorCode: z.string(),
      }),
    },
  }),
  handler: userController.createUser,
});

Using Standard Responses

The standardResponses utility generates standard response schemas for common operations:

JavaScript
const { standardResponses } = fastify.diScope.resolve('utils');

// Define your entity schema
const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(3).max(100),
  email: z.string().email(),
});

// Generate standard responses
const responses = standardResponses(userSchema);

// Use in your routes
fastify.route({
  method: 'GET',
  url: '/users/:id',
  ...documentProtectedRoute({
    tag: 'Users',
    summary: 'Get user by ID',
    description: 'Returns a single user by their ID',
    params: z.object({
      id: z.string().uuid(),
    }),
    response: responses.get, // 200, 404, 500
  }),
  handler: userController.getUserById,
});

fastify.route({
  method: 'POST',
  url: '/users',
  ...documentProtectedRoute({
    tag: 'Users',
    summary: 'Create a new user',
    description: 'Creates a new user with the provided data',
    body: createUserSchema,
    response: responses.create, // 201, 400, 422, 500
  }),
  handler: userController.createUser,
});

Available standard responses:

  • responses.get: For GET requests (200, 404, 500)
  • responses.list: For GET list requests (200, 500)
  • responses.create: For POST requests (201, 400, 422, 500)
  • responses.update: For PUT requests (200, 400, 404, 422, 500)
  • responses.delete: For DELETE requests (204, 404, 500)
  • responses.custom: For custom responses (errorResponse, successResponse, listResponse)

Complete Example

Here's a complete example of a route file with documentation:

JavaScript
const { z } = require('zod');

async function userRoutes(fastify) {
  const {
    documentPublicRoute,
    documentProtectedRoute,
    standardResponses
  } = fastify.diScope.resolve('utils');

  // Entity schema
  const userSchema = z.object({
    id: z.string().uuid(),
    name: z.string().min(3).max(100),
    email: z.string().email(),
    role: z.enum(['admin', 'user']),
    createdAt: z.string().datetime(),
    updatedAt: z.string().datetime(),
  });

  // Request schemas
  const createUserSchema = z.object({
    name: z.string().min(3).max(100),
    email: z.string().email(),
    password: z.string().min(8),
    role: z.enum(['admin', 'user']).default('user'),
  });

  const updateUserSchema = createUserSchema.partial();

  const userParamsSchema = z.object({
    id: z.string().uuid(),
  });

  const userQuerySchema = z.object({
    role: z.enum(['admin', 'user']).optional(),
    page: z.string().regex(/^\d+$/).transform(Number).optional(),
    limit: z.string().regex(/^\d+$/).transform(Number).optional(),
  });

  // Generate standard responses
  const responses = standardResponses(userSchema);

  // Routes
  fastify.route({
    method: 'GET',
    url: '/users',
    preHandler: fastify.authenticate,
    ...documentProtectedRoute({
      tag: 'Users',
      summary: 'Get all users',
      description: 'Returns a list of users with pagination',
      querystring: userQuerySchema,
      response: responses.list,
    }),
    handler: userController.getUsers,
  });

  fastify.route({
    method: 'GET',
    url: '/users/:id',
    preHandler: fastify.authenticate,
    ...documentProtectedRoute({
      tag: 'Users',
      summary: 'Get user by ID',
      description: 'Returns a single user by their ID',
      params: userParamsSchema,
      response: responses.get,
    }),
    handler: userController.getUserById,
  });

  fastify.route({
    method: 'POST',
    url: '/users',
    preHandler: fastify.authenticate,
    ...documentProtectedRoute({
      tag: 'Users',
      summary: 'Create a new user',
      description: 'Creates a new user with the provided data',
      body: createUserSchema,
      response: responses.create,
    }),
    handler: userController.createUser,
  });

  fastify.route({
    method: 'PUT',
    url: '/users/:id',
    preHandler: fastify.authenticate,
    ...documentProtectedRoute({
      tag: 'Users',
      summary: 'Update a user',
      description: 'Updates an existing user with the provided data',
      params: userParamsSchema,
      body: updateUserSchema,
      response: responses.update,
    }),
    handler: userController.updateUser,
  });

  fastify.route({
    method: 'DELETE',
    url: '/users/:id',
    preHandler: fastify.authenticate,
    ...documentProtectedRoute({
      tag: 'Users',
      summary: 'Delete a user',
      description: 'Deletes an existing user',
      params: userParamsSchema,
      response: responses.delete,
    }),
    handler: userController.deleteUser,
  });
}

module.exports = userRoutes;

Accessing the Documentation

The API documentation is available at the /api-docs endpoint in non-production environments. It provides a user-friendly interface to explore and test the API.

Best Practices

  1. Be Descriptive: Provide clear summaries and descriptions for your routes
  2. Use Tags: Group related routes under the same tag
  3. Document All Parameters: Document all request parameters, including body, query, and path parameters
  4. Document Responses: Document all possible responses, including error responses
  5. Use Standard Responses: Use the standardResponses utility for consistent response documentation
  6. Keep Schemas DRY: Define schemas once and reuse them across routes
  7. Test Documentation: Verify that the documentation is accurate by testing the API through the Swagger UI

Troubleshooting

If your documentation is not showing up correctly:

  1. Make sure you're using the correct documentation utility for your route
  2. Check that your Zod schemas are correctly defined
  3. Verify that the route is registered correctly
  4. Check the browser console for any errors

Advanced Usage

Custom Response Schemas

You can create custom response schemas for specific routes:

JavaScript
const { z } = require('zod');

// Custom response schema
const loginResponseSchema = z.object({
  success: z.boolean(),
  message: z.string(),
  data: z.object({
    token: z.string(),
    refreshToken: z.string(),
    user: userSchema,
  }),
});

// Use in route
fastify.route({
  method: 'POST',
  url: '/login',
  ...documentPublicRoute({
    tag: 'Auth',
    summary: 'Login',
    description: 'Authenticates a user and returns a token',
    body: loginSchema,
    response: {
      200: loginResponseSchema,
      400: responses.custom.errorResponse,
      401: responses.custom.errorResponse,
    },
  }),
  handler: authController.login,
});

Security Requirements

You can specify custom security requirements for a route:

JavaScript
fastify.route({
  method: 'GET',
  url: '/public-data',
  ...documentRoute({
    tag: 'Data',
    summary: 'Get public data',
    description: 'Returns public data without authentication',
    response: responses.get,
    security: [], // No security requirements
  }),
  handler: dataController.getPublicData,
});

Conclusion

Adding OpenAPI documentation to your routes is a simple process that provides significant benefits:

  1. Self-Documentation: The API documents itself as you develop
  2. Testing: The Swagger UI provides a convenient way to test your API
  3. Client Generation: The OpenAPI specification can be used to generate client libraries
  4. Consistency: The documentation utilities ensure consistent documentation across the API

By following this guide, you can ensure that your API is well-documented and easy to use.