Development Docs

API Documentation Generation

This document explains how to generate and use the OpenAPI specification for the DeployStack Backend API.

Overview

The DeployStack Backend uses Fastify with Swagger plugins to automatically generate OpenAPI 3.0 specifications. Route schemas are defined using Zod for type safety and expressiveness, and then converted to JSON Schema using the zod-openapi library. This provides:

  • Interactive Documentation: Swagger UI interface for testing APIs
  • Postman Integration: JSON/YAML specs that can be imported into Postman
  • Automated Generation: Specifications are generated from actual route code

🔒 Security First

IMPORTANT: Before developing any protected API endpoints, read the API Security Best Practices documentation. It covers critical security patterns including:

  • Authorization Before Validation: Why preValidation must be used instead of preHandler for authorization
  • Proper Error Responses: Ensuring unauthorized users get 403 Forbidden, not validation errors
  • Security Testing: How to test authorization properly
  • Common Pitfalls: Security anti-patterns to avoid

Key Rule: Always use preValidation for authorization checks to prevent information disclosure to unauthorized users.

🔐 Dual Authentication Support

The DeployStack Backend supports dual authentication for API endpoints, allowing both web users (cookie-based) and CLI users (OAuth2 Bearer tokens) to access the same endpoints seamlessly.

Authentication Methods

  1. Cookie Authentication (Web Users)

    • Session-based authentication using HTTP cookies
    • Automatic for web browser requests
    • Uses session cookie set during login
  2. OAuth2 Bearer Token Authentication (CLI Users)

    • RFC 6749 compliant OAuth2 implementation with PKCE
    • Uses Authorization: Bearer <token> header
    • Scope-based access control

Dual Authentication Middleware

Use these middleware functions to enable dual authentication on endpoints:

import { requireAuthenticationAny, requireOAuthScope } from '../../middleware/oauthMiddleware';

fastify.get('/your-endpoint', {
  schema: {
    security: [
      { cookieAuth: [] },    // Cookie authentication
      { bearerAuth: [] }     // OAuth2 Bearer token
    ]
  },
  preValidation: [
    requireAuthenticationAny(),     // Accept either auth method
    requireOAuthScope('your:scope') // Enforce OAuth2 scope
  ]
}, async (request, reply) => {
  // Endpoint accessible via both authentication methods
  const authType = request.tokenPayload ? 'oauth2' : 'cookie';
  const userId = request.user!.id;
});

OAuth2 Scopes

Available OAuth2 scopes for fine-grained access control:

  • mcp:read - Read MCP server installations and configurations
  • account:read - Read account information
  • user:read - Read user profile information
  • teams:read - Read team memberships and information
  • offline_access - Maintain access when not actively using the application

OAuth2 Flow Endpoints

  • GET /api/oauth2/auth - Authorization endpoint (PKCE required)
  • GET /api/oauth2/consent - User consent page
  • POST /api/oauth2/consent - Process consent decision
  • POST /api/oauth2/token - Token exchange endpoint

Client Configuration

  • Client ID: deploystack-gateway-cli
  • Redirect URIs: http://localhost:8976/oauth/callback, http://127.0.0.1:8976/oauth/callback
  • PKCE: Required (SHA256 method)
  • Token Lifetime: 1 hour access tokens, 30 day refresh tokens

Usage Examples

Web Users (Cookie Authentication):

curl -b cookies.txt "http://localhost:3000/api/teams/me/default"

CLI Users (OAuth2 Bearer Token):

curl -H "Authorization: Bearer <access_token>" \
     "http://localhost:3000/api/teams/me/default"

Available Commands

1. Generate Complete API Specification

npm run api:spec

This command:

  • Starts a temporary server
  • Generates both JSON and YAML specifications
  • Saves files to api-spec.json and api-spec.yaml
  • Provides URLs for interactive documentation
  • Automatically shuts down the server

Output:

  • api-spec.json - OpenAPI JSON specification (for Postman import)
  • api-spec.yaml - OpenAPI YAML specification

2. Generate JSON Specification (requires running server)

npm run api:spec:json

Requires the development server to be running (npm run dev).

3. Generate YAML Specification (requires running server)

npm run api:spec:yaml

Requires the development server to be running (npm run dev).

Usage Examples

cd services/backend
npm run api:spec

Manual Generation with Running Server

# Terminal 1: Start the server
cd services/backend
npm run dev

# Terminal 2: Generate specifications
npm run api:spec:json
npm run api:spec:yaml

Accessing Documentation

When the server is running (npm run dev), you can access:

Importing into Postman

  1. Run npm run api:spec to generate the specification
  2. Open Postman
  3. Click "Import"
  4. Select the generated api-spec.json file
  5. All API endpoints will be imported with proper documentation

Route File Structure Rules

IMPORTANT: Every new API endpoint must be created in a separate file following the established directory structure pattern. Do not add route definitions directly to src/routes/index.ts.

File Structure Requirements

  1. Separate Files: Each route or group of related routes must be in its own file
  2. Directory Organization: Group related routes in directories (e.g., /auth/, /users/, /health/)
  3. Import Pattern: Routes are imported and registered in src/routes/index.ts
  4. Consistent Naming: Use descriptive names that match the route purpose
  5. Modular Approach: Keep route files small and focused - aim for 1-3 related methods per file maximum
  6. Maintainability: Avoid large monolithic route files that become difficult to maintain

Correct File Structure

services/backend/src/routes/
├── index.ts              # Main routes registration (imports only)
├── health/
│   └── index.ts          # Health check endpoints
├── auth/
│   ├── loginEmail.ts     # Email login endpoint
│   ├── registerEmail.ts  # Email registration endpoint
│   └── logout.ts         # Logout endpoint
├── db/
│   ├── status.ts         # Database status endpoint
│   └── setup.ts          # Database setup endpoint
├── users/
│   └── index.ts          # User management endpoints
└── teams/
    └── index.ts          # Team management endpoints

For complex feature areas, break down routes into smaller, focused files:

services/backend/src/routes/mcp/
├── index.ts              # Route registration only
├── categories/
│   ├── create.ts        # POST /api/mcp/categories (1 method)
│   ├── update.ts        # PUT /api/mcp/categories/{id} (1 method)
│   └── delete.ts        # DELETE /api/mcp/categories/{id} (1 method)
├── servers/
│   ├── list.ts          # GET /api/mcp/servers (1 method)
│   ├── get.ts           # GET /api/mcp/servers/{id} (1 method)
│   ├── search.ts        # GET /api/mcp/servers/search (1 method)
│   ├── create-global.ts # POST /api/mcp/servers/global (1 method)
│   ├── update-global.ts # PUT /api/mcp/servers/global/{id} (1 method)
│   └── delete-global.ts # DELETE /api/mcp/servers/global/{id} (1 method)
└── versions/
    ├── list.ts          # GET /api/mcp/servers/{id}/versions (1 method)
    ├── create.ts        # POST /api/mcp/servers/{id}/versions (1 method)
    └── update.ts        # PUT /api/mcp/servers/{id}/versions/{versionId} (1 method)

Benefits of Modular Approach:

  • Easier Maintenance: Small files are easier to understand and modify
  • Better Testing: Individual route files can be tested in isolation
  • Team Collaboration: Multiple developers can work on different routes without conflicts
  • Clear Responsibility: Each file has a single, clear purpose
  • Reduced Complexity: Avoid hundreds of lines in single files

Route File Template

Each route file should follow this pattern:

import { type FastifyInstance } from 'fastify'
import { z } from 'zod'
import { createSchema } from 'zod-openapi'

// Define your schemas
const responseSchema = z.object({
  // Your response structure
});

export default async function yourRoute(server: FastifyInstance) {
  server.get('/your-endpoint', {
    schema: {
      tags: ['Your Category'],
      summary: 'Brief description',
      description: 'Detailed description',
      response: {
        200: createSchema(responseSchema)
      }
    }
  }, async () => {
    // Your route logic
    return { /* your response */ }
  });
}

Registration in index.ts

Import and register your route in src/routes/index.ts:

// Import your route
import yourRoute from './your-directory'

export const registerRoutes = (server: FastifyInstance): void => {
  server.register(async (apiInstance) => {
    // Register your route
    await apiInstance.register(yourRoute);
    
    // Other route registrations...
  }, { prefix: '/api' });
}

❌ What NOT to Do

// DON'T: Add routes directly to index.ts
export const registerRoutes = (server: FastifyInstance): void => {
  server.register(async (apiInstance) => {
    // ❌ BAD: Inline route definition
    apiInstance.get('/my-endpoint', {
      schema: { /* ... */ }
    }, async () => {
      return { message: 'This should be in a separate file!' }
    });
  }, { prefix: '/api' });
}

✅ What TO Do

// ✅ GOOD: Import and register separate route files
import myEndpointRoute from './my-endpoint'

export const registerRoutes = (server: FastifyInstance): void => {
  server.register(async (apiInstance) => {
    // ✅ GOOD: Register imported route
    await apiInstance.register(myEndpointRoute);
  }, { prefix: '/api' });
}

Benefits of This Structure

  1. Maintainability: Each endpoint is self-contained and easy to find
  2. Scalability: Adding new endpoints doesn't clutter the main routes file
  3. Testing: Individual route files can be tested in isolation
  4. Code Organization: Related functionality is grouped together
  5. Team Collaboration: Multiple developers can work on different routes without conflicts

Content-Type Header Requirements

When to Include Content-Type Headers

IMPORTANT: The Content-Type: application/json header is required for specific HTTP methods when sending request body data.

✅ ALWAYS Include Content-Type for:

  • POST requests with request body data
  • PUT requests with request body data
  • PATCH requests with request body data

❌ NEVER Include Content-Type for:

  • GET requests (no request body)
  • DELETE requests (typically no request body)
  • HEAD requests (no request body)

Correct Client Implementation Pattern

function makeRequest(method, path, data = null, cookies = null) {
  const options = {
    method,
    headers: { 'Accept': 'application/json' }
  };

  // Set Content-Type for methods that send request body data
  if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) && data !== null) {
    options.headers['Content-Type'] = 'application/json';
  }

  // Rest of implementation...
}

❌ Problematic Pattern (Avoid This)

// UNCLEAR: This doesn't indicate WHICH methods need Content-Type
if (data) {
  options.headers['Content-Type'] = 'application/json';
}

API Specification Content-Type Documentation

When defining route schemas, explicitly document Content-Type requirements for POST/PUT/PATCH endpoints:

// For endpoints that require Content-Type
const routeSchema = {
  tags: ['Category'],
  summary: 'Create new item',
  description: 'Creates a new item. Requires Content-Type: application/json header when sending request body.',
  requestBody: {
    required: true,
    content: {
      'application/json': {
        schema: createSchema(requestSchema)
      }
    }
  },
  // ... rest of schema
};

Adding Documentation to Routes

To add OpenAPI documentation to your routes, define your request body and response schemas using Zod. Then, use the createSchema utility to convert these Zod schemas into the JSON Schema format expected by Fastify.

Make sure you have zod and zod-openapi installed in your backend service.

IMPORTANT: After the Zod v4 migration, we use a dual-schema approach to ensure both proper Fastify validation and accurate OpenAPI documentation.

import { z } from 'zod';
import { createSchema } from 'zod-openapi';

// 1. Define your Zod schemas for request body, responses, etc.
const myRequestBodySchema = z.object({
  name: z.string().min(3).describe("The name of the item (min 3 chars)"),
  count: z.number().positive().describe("How many items (must be positive)"),
  type: z.enum(['mysql', 'sqlite']).describe("Database engine type")
});

const mySuccessResponseSchema = z.object({
  success: z.boolean().describe("Indicates if the operation was successful"),
  itemId: z.string().uuid().describe("The UUID of the created/affected item"),
  message: z.string().optional().describe("Optional success message")
});

const myErrorResponseSchema = z.object({
  success: z.boolean().default(false).describe("Indicates failure"),
  error: z.string().describe("Error message detailing what went wrong")
});

// 2. Construct the Fastify route schema using DUAL-SCHEMA PATTERN
const routeSchema = {
  tags: ['Category'], // Your API category
  summary: 'Brief description of your endpoint',
  description: 'Detailed description of what this endpoint does, its parameters, and expected outcomes. Requires Content-Type: application/json header when sending request body.',
  security: [{ cookieAuth: [] }], // Include if authentication is required
  
  // ✅ CRITICAL: Use plain JSON Schema for Fastify validation
  body: {
    type: 'object',
    properties: {
      name: { type: 'string', minLength: 3 },
      count: { type: 'number', minimum: 1 },
      type: { type: 'string', enum: ['mysql', 'sqlite'] }
    },
    required: ['name', 'count', 'type'],
    additionalProperties: false
  },
  
  // ✅ Use createSchema() for OpenAPI documentation
  requestBody: {
    required: true,
    content: {
      'application/json': {
        schema: createSchema(myRequestBodySchema)
      }
    }
  },
  response: {
    200: createSchema(mySuccessResponseSchema.describe("Successful operation")),
    400: createSchema(myErrorResponseSchema.describe("Bad Request - Invalid input")),
    // Define other responses (e.g., 401, 403, 404, 500) similarly
  }
};

// 3. Use the schema in your Fastify route definition with proper TypeScript typing
interface RequestBody {
  name: string;
  count: number;
  type: 'mysql' | 'sqlite';
}

fastify.post<{ Body: RequestBody }>(
  '/your-route', 
  { schema: routeSchema }, 
  async (request, reply) => {
    // ✅ Fastify has already validated request.body using the JSON schema
    // ✅ If we reach here, request.body is guaranteed to be valid
    // ✅ No manual validation needed!
    
    const { name, count, type } = request.body; // Fully typed and validated
    
    // Your route handler logic here
    const successResponse = { 
      success: true, 
      itemId: 'some-uuid-v4-here', 
      message: `Item ${name} processed successfully with ${count} items using ${type}.` 
    };
    const jsonString = JSON.stringify(successResponse);
    return reply.status(200).type('application/json').send(jsonString);
  }
);

Key Benefits of This Approach

  1. Single Source of Truth: Zod schemas define both validation AND documentation
  2. Automatic Validation: Fastify automatically validates requests before your handler runs
  3. No Manual Validation: Remove all manual zod.parse() calls and field checks
  4. Better Error Messages: Fastify provides detailed validation errors automatically
  5. Type Safety: Handlers receive properly typed, validated data
  6. Cleaner Code: No redundant validation logic in handlers

JSON Response Serialization Pattern

CRITICAL: After the Zod v4 migration, all API responses must use manual JSON serialization to prevent "[object Object]" serialization issues.

Required Response Pattern

// ✅ CORRECT: Manual JSON serialization
const successResponse = {
  success: true,
  message: 'Operation completed successfully',
  data: { /* your data */ }
};
const jsonString = JSON.stringify(successResponse);
return reply.status(200).type('application/json').send(jsonString);

What NOT to Do

// ❌ WRONG: Direct object response (causes serialization issues)
return reply.status(200).send({
  success: true,
  message: 'This will become "[object Object]"'
});

// ❌ WRONG: Using reply.send() without JSON.stringify()
const response = { success: true, message: 'Test' };
return reply.status(200).send(response);

Error Response Pattern

All error responses must also use manual JSON serialization:

// ✅ CORRECT: Error response with manual serialization
const errorResponse = {
  success: false,
  error: 'Detailed error message'
};
const jsonString = JSON.stringify(errorResponse);
return reply.status(400).type('application/json').send(jsonString);

Authentication Middleware Pattern

Authentication middleware and hooks must also use this pattern:

// ✅ CORRECT: Authentication error with manual serialization
const errorResponse = {
  success: false,
  error: 'Unauthorized: Authentication required.'
};
const jsonString = JSON.stringify(errorResponse);
return reply.status(401).type('application/json').send(jsonString);

Why This Pattern is Required

After the Zod v4 migration, Fastify's automatic JSON serialization can fail with complex objects, resulting in:

  • Response bodies showing "[object Object]" instead of actual data
  • Client applications receiving unparseable responses
  • Test failures due to missing success and error properties

The manual JSON serialization pattern ensures:

  • ✅ Consistent, parseable JSON responses
  • ✅ Proper success/error properties in all responses
  • ✅ Reliable client-server communication
  • ✅ Passing e2e tests

Why Both body and requestBody Properties?

Important: You need BOTH properties for complete functionality:

  • body: Enables Fastify's automatic request validation using the Zod schema
  • requestBody: Ensures proper OpenAPI specification generation with Content-Type documentation

Without body, validation won't work. Without requestBody, your API specification won't properly document the application/json Content-Type requirement.

What NOT to Do (Anti-patterns)

Don't do manual validation in handlers:

// BAD: Manual validation (redundant)
const parsedBody = myRequestBodySchema.safeParse(request.body);
if (!parsedBody.success) {
  return reply.status(400).send({ error: 'Invalid request body' });
}

// BAD: Manual field checks (redundant)
if (!request.body.name || !request.body.count) {
  return reply.status(400).send({ error: 'Required fields missing' });
}

// BAD: Manual enum validation (redundant)
if (request.body.type !== 'mysql' && request.body.type !== 'sqlite') {
  return reply.status(400).send({ error: 'Invalid database type' });
}

Do trust Fastify's automatic validation:

// GOOD: Trust the validation - if handler runs, data is valid
const { name, count, type } = request.body; // Already validated by Fastify

Validation Flow

The validation chain works as follows:

Zod Schema → JSON Schema → Fastify Validation → Handler

  1. Zod Schema: Define validation rules using Zod
  2. JSON Schema: Convert to OpenAPI format using createSchema()
  3. Fastify Validation: Fastify automatically validates incoming requests
  4. Handler: Receives validated, typed data

If validation fails, Fastify automatically returns a 400 error before your handler runs.

Real-World Examples

See these files for complete examples of proper Zod validation:

  • src/routes/db/setup.ts - Database setup with enum validation
  • src/routes/db/status.ts - Simple GET endpoint with response schemas
  • src/routes/auth/loginEmail.ts - Login with required string fields
  • src/routes/auth/registerEmail.ts - Registration with complex validation rules

Note: Older examples in this document (like the "Logout Route Documentation" below) might still show manually crafted JSON schemas. The recommended approach is now to use Zod with automatic Fastify validation as shown above.

Example: Logout Route Documentation

The logout route (/api/auth/logout) demonstrates proper documentation:

const logoutSchema = {
  tags: ['Authentication'],
  summary: 'User logout',
  description: 'Invalidates the current user session and clears authentication cookies',
  security: [{ cookieAuth: [] }],
  response: {
    200: {
      type: 'object',
      properties: {
        success: { 
          type: 'boolean',
          description: 'Indicates if the logout operation was successful'
        },
        message: { 
          type: 'string',
          description: 'Human-readable message about the logout result'
        }
      },
      required: ['success', 'message'],
      examples: [
        {
          success: true,
          message: 'Logged out successfully.'
        }
      ]
    }
  }
};

Configuration

Fastify Server Configuration

The Fastify server is configured with custom AJV options to ensure compatibility with zod-openapi schema generation. This configuration is in src/server.ts:

const server = fastify({
  logger: loggerConfig,
  disableRequestLogging: true,
  ajv: {
    customOptions: {
      strict: false,        // Allows unknown keywords in schemas
      strictTypes: false,   // Disables strict type checking  
      strictTuples: false   // Disables strict tuple checking
    }
  }
})

Why these AJV options are required:

  • strict: false: AJV v8+ runs in strict mode by default, which rejects schemas containing unknown keywords. The zod-openapi library generates schemas that may include keywords AJV doesn't recognize in strict mode.
  • strictTypes: false: Prevents strict type validation errors that can occur with complex Zod schemas.
  • strictTuples: false: Allows more flexible tuple handling for array schemas.

Important: These settings don't affect validation behavior - they only allow the schema compilation to succeed. All validation rules defined in your Zod schemas still work exactly as expected.

Swagger Configuration

The Swagger documentation configuration is also in src/server.ts:

await server.register(fastifySwagger, {
  openapi: {
    openapi: '3.0.0',
    info: {
      title: 'DeployStack Backend API',
      description: 'API documentation for DeployStack Backend',
      version: '0.20.5'
    },
    servers: [
      {
        url: 'http://localhost:3000',
        description: 'Development server'
      }
    ],
    components: {
      securitySchemes: {
        cookieAuth: {
          type: 'apiKey',
          in: 'cookie',
          name: 'auth_session'
        }
      }
    }
  }
});

Troubleshooting

"Route already declared" Error

This happens when trying to manually add routes that Swagger UI already provides. The /documentation/json and /documentation/yaml endpoints are automatically created.

"Failed to fetch API spec" Error

Ensure the server is fully started before trying to fetch the specification. The generation script includes a 2-second delay to allow for complete initialization.

Missing Route Documentation

Routes without schema definitions will appear in the specification but with minimal documentation. Add schema objects to routes for complete documentation.

Next Steps

To extend API documentation:

  1. Add schema definitions to more routes
  2. Define reusable components in the OpenAPI configuration
  3. Add request body schemas for POST/PUT endpoints
  4. Include error response schemas (400, 401, 500, etc.)
  5. Add parameter validation schemas

Plugin API Routes

Plugin Route Structure

All plugin routes are automatically namespaced under /api/plugin/<plugin-name>/ for security and isolation:

  • Core Routes: /api/auth/*, /api/users/*, /api/settings/* (protected from plugins)
  • Plugin Routes: /api/plugin/<plugin-name>/* (isolated per plugin)

Example Plugin Routes

For a plugin with ID example-plugin:

GET    /api/plugin/example-plugin/examples
GET    /api/plugin/example-plugin/examples/:id
POST   /api/plugin/example-plugin/examples
PUT    /api/plugin/example-plugin/examples/:id
DELETE /api/plugin/example-plugin/examples/:id

Security Benefits

  1. Route Isolation: Plugins cannot interfere with core routes or each other
  2. Predictable Structure: All plugin APIs follow the same pattern
  3. Easy Identification: Plugin ownership is clear from the URL
  4. Automatic Namespacing: No manual prefix management required

Plugin Route Registration

Plugins register routes using the PluginRouteManager:

// In plugin's routes.ts file
export async function registerRoutes(routeManager: PluginRouteManager, db: AnyDatabase | null) {
  // This becomes /api/plugin/my-plugin/data
  routeManager.get('/data', async () => {
    return { message: 'Hello from plugin!' };
  });
}

Files Generated

  • api-spec.json - Complete OpenAPI 3.0 specification in JSON format
  • api-spec.yaml - Complete OpenAPI 3.0 specification in YAML format
  • Interactive documentation available at /documentation when server is running

On this page

API Documentation GenerationOverview🔒 Security First🔐 Dual Authentication SupportAuthentication MethodsDual Authentication MiddlewareOAuth2 ScopesOAuth2 Flow EndpointsClient ConfigurationUsage ExamplesAvailable Commands1. Generate Complete API Specification2. Generate JSON Specification (requires running server)3. Generate YAML Specification (requires running server)Usage ExamplesComplete Generation (Recommended)Manual Generation with Running ServerAccessing DocumentationImporting into PostmanRoute File Structure RulesFile Structure RequirementsCorrect File StructureModular Route Organization (Recommended)Route File TemplateRegistration in index.ts❌ What NOT to Do✅ What TO DoBenefits of This StructureContent-Type Header RequirementsWhen to Include Content-Type Headers✅ ALWAYS Include Content-Type for:❌ NEVER Include Content-Type for:Correct Client Implementation Pattern❌ Problematic Pattern (Avoid This)API Specification Content-Type DocumentationAdding Documentation to RoutesRecommended Approach: Dual-Schema Pattern for Validation + DocumentationKey Benefits of This ApproachJSON Response Serialization PatternRequired Response PatternWhat NOT to DoError Response PatternAuthentication Middleware PatternWhy This Pattern is RequiredWhy Both body and requestBody Properties?What NOT to Do (Anti-patterns)Validation FlowZod Schema → JSON Schema → Fastify Validation → HandlerReal-World ExamplesExample: Logout Route DocumentationConfigurationFastify Server ConfigurationSwagger ConfigurationTroubleshooting"Route already declared" Error"Failed to fetch API spec" ErrorMissing Route DocumentationNext StepsPlugin API RoutesPlugin Route StructureExample Plugin RoutesSecurity BenefitsPlugin Route RegistrationFiles Generated