JSandy Logo

JSandy

docs

Backend

Documenting your API

The .describe() method allows you to add comprehensive documentation metadata to your procedures, which can be used to generate OpenAPI specifications and enhance your API documentation.

Basic Usage

Add documentation to any procedure using the .describe() method:

import { z } from "zod";
import { jsandy } from "@jsandy/rpc";

const { router, procedure } = jsandy.init();

const userRouter = router({
  getUser: procedure
    .input(z.object({ id: z.string() }))
    .describe({
      description: "Retrieves a user by their unique identifier",
      summary: "Get user by ID",
      schema: z.object({
        id: z.string(),
        name: z.string(),
        email: z.string(),
        createdAt: z.date(),
      }),
      tags: ["users"],
      operationId: "getUserById"
    })
    .get(({ input, c }) => {
      // Implementation
      return c.json({
        id: input.id,
        name: "John Doe",
        email: "john@example.com",
        createdAt: new Date()
      });
    })
});

Description Options

The .describe() method accepts a comprehensive configuration object:

interface ProcedureDescription {
  /** Human-readable description of what this endpoint does */
  description: string;

  /** Optional summary for the endpoint (shorter than description) */
  summary?: string;

  /** Zod schema defining the expected output/response structure */
  schema?: ZodAny;

  /** Optional tags for grouping endpoints in documentation */
  tags?: string[];

  /** Optional operation ID for OpenAPI specification */
  operationId?: string;

  /** Whether this endpoint is deprecated */
  deprecated?: boolean;

  /** Additional OpenAPI metadata */
  openapi?: {
    /** Security requirements for this endpoint */
    security?: Array<Record<string, string[]>>;

    /** Additional response definitions */
    responses?: Record<string, any>;

    /** Request body examples */
    examples?: Record<string, any>;
  };
}

Working with Different HTTP Methods

GET Endpoints

For GET endpoints, input schemas become query parameters in the OpenAPI spec:

const searchPosts = procedure
  .input(z.object({
    query: z.string().min(1),
    limit: z.number().min(1).max(100).default(10),
    category: z.enum(["tech", "business", "lifestyle"]).optional()
  }))
  .describe({
    description: "Search blog posts with pagination and filtering",
    summary: "Search posts",
    schema: z.object({
      posts: z.array(z.object({
        id: z.string(),
        title: z.string(),
        excerpt: z.string()
      })),
      pagination: z.object({
        total: z.number(),
        hasNext: z.boolean()
      })
    }),
    tags: ["posts", "search"],
    operationId: "searchPosts"
  })
  .get(({ input, c }) => {
    // Implementation
  });

POST Endpoints

For POST endpoints, input schemas become request body specifications:

const createPost = procedure
  .input(z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(10),
    tags: z.array(z.string()).optional(),
    publishAt: z.date().optional()
  }))
  .describe({
    description: "Creates a new blog post with the provided content",
    summary: "Create blog post",
    schema: z.object({
      id: z.string(),
      slug: z.string(),
      status: z.enum(["draft", "published"]),
      createdAt: z.date()
    }),
    tags: ["posts", "content"],
    operationId: "createPost"
  })
  .post(({ input, c }) => {
    // Implementation
  });

Advanced OpenAPI Features

Security Requirements

Add authentication requirements to specific endpoints:

const getAdminStats = procedure
  .describe({
    description: "Retrieves system statistics (admin only)",
    summary: "Get admin statistics",
    schema: z.object({
      totalUsers: z.number(),
      systemHealth: z.string()
    }),
    tags: ["admin"],
    openapi: {
      security: [{ bearerAuth: [] }]
    }
  })
  .get(({ c }) => {
    // Implementation
  });

Custom Error Responses

Define additional response codes and schemas:

const deleteUser = procedure
  .input(z.object({ id: z.string() }))
  .describe({
    description: "Permanently deletes a user account",
    summary: "Delete user",
    tags: ["users"],
    openapi: {
      responses: {
        404: {
          description: "User not found",
          content: {
            "application/json": {
              schema: {
                type: "object",
                properties: {
                  error: { type: "string", example: "USER_NOT_FOUND" },
                  message: { type: "string" }
                }
              }
            }
          }
        },
        409: {
          description: "Cannot delete user with active subscriptions",
          content: {
            "application/json": {
              schema: {
                type: "object",
                properties: {
                  error: { type: "string", example: "USER_HAS_ACTIVE_SUBSCRIPTIONS" },
                  subscriptions: { type: "array", items: { type: "string" } }
                }
              }
            }
          }
        }
      }
    }
  })
  .post(({ input, c }) => {
    // Implementation
  });

Generating OpenAPI Documentation

Automatic OpenAPI Generation

Use the built-in generator to create OpenAPI specifications:

import { generateOpenAPISpec, addOpenAPIRoutes } from "@jsandy/rpc";
import { Hono } from "hono";

const app = new Hono();

// Add automatic documentation routes
await addOpenAPIRoutes(app, userRouter, {
  title: "User Management API",
  version: "1.0.0",
  description: "Comprehensive user management with authentication",
  servers: [
    { url: "https://api.example.com", description: "Production" },
    { url: "http://localhost:8080", description: "Development" }
  ],
  securitySchemes: {
    bearerAuth: {
      type: "http",
      scheme: "bearer",
      bearerFormat: "JWT"
    }
  }
}, {
  specPath: "/openapi.json",  // OpenAPI spec endpoint
  docsPath: "/docs"           // Swagger UI endpoint
});

Manual OpenAPI Generation

For more control, generate the spec manually:

const openAPISpec = await generateOpenAPISpec(userRouter, {
  title: "My API",
  version: "2.0.0",
  description: "API documentation",
  basePath: "/api/v2"
});

// Serve the specification
app.get("/api-spec.json", (c) => c.json(openAPISpec));

// Custom Swagger UI with additional configuration
app.get("/api-docs", (c) => {
  const html = createSwaggerUI("/api-spec.json", "My API Docs");
  return c.html(html);
});

Method Chaining

The .describe() method works seamlessly with method chaining:

const complexProcedure = procedure
  .input(userInputSchema)           // Add input validation
  .describe({                       // Add documentation
    description: "Complex operation",
    tags: ["complex"]
  })
  .use(authMiddleware)              // Add authentication
  .use(rateLimitMiddleware)         // Add rate limiting
  .get(({ input, ctx, c }) => {     // Define handler
    // Implementation with full type safety
  });

Accessing Documentation Metadata

Retrieve documentation metadata programmatically:

// Get all operations with their descriptions
const operations = userRouter.getAllOperations();

// Get description for a specific operation
const getUserDescription = userRouter.getOperationDescription("getUser");

console.log(getUserDescription?.description);
console.log(getUserDescription?.tags);

Integration with @hono/zod-openapi

The .describe() method is designed to work alongside @hono/zod-openapi for maximum compatibility:

Note: Most examples use zod/v4 for standard schemas. The @hono/zod-openapi import provides additional OpenAPI-specific enhancements and should be used when you need advanced OpenAPI metadata on your schemas.

// Use enhanced Zod schemas with OpenAPI metadata
import { z } from '@hono/zod-openapi';

const UserSchema = z.object({
  id: z.string().openapi({ example: '123' }),
  name: z.string().openapi({ example: 'John Doe' }),
  email: z.string().email().openapi({ example: 'john@example.com' })
}).openapi('User');

const getUserProcedure = procedure
  .input(z.object({
    id: z.string().openapi({
      param: { name: 'id', in: 'path' },
      example: '123'
    })
  }))
  .describe({
    description: "Get user with enhanced OpenAPI metadata",
    schema: UserSchema,
    tags: ["users"]
  })
  .get(({ input, c }) => {
    // Implementation
  });