Docus Logo
Charcole Swagger

Swagger Examples

Practical examples of using @charcoles/swagger for common API patterns.

Simple GET endpoint

The most basic example. A GET endpoint that returns data.

/**
 * @swagger
 * /api/health:
 *   get:
 *     summary: Health check
 *     tags:
 *       - System
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 */
router.get(
  "/health",
  asyncHandler(async (req, res) => {
    res.json({ success: true, message: "Server is healthy" });
  }),
);

9 lines of documentation. No schemas to register because there's no request body. Response template handles the 200 status automatically.


POST endpoint with validation

The most common case. Creating a resource with Zod validation.

Step 1: Define the Zod schema

// src/modules/posts/posts.schemas.ts
export const createPostSchema = z.object({
  body: z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(1),
    published: z.boolean().default(false),
  }),
});

Step 2: Register it in Swagger config

// src/config/swagger.config.ts
import { createPostSchema } from "../modules/posts/posts.schemas.ts";

const swaggerConfig = {
  schemas: {
    createPostSchema, // Auto-converted!
  },
};

Step 3: Document the route

/**
 * @swagger
 * /api/posts:
 *   post:
 *     summary: Create a new post
 *     tags:
 *       - Posts
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/createPostSchema'
 *     responses:
 *       201:
 *         $ref: '#/components/responses/Success'
 *       400:
 *         $ref: '#/components/responses/ValidationError'
 */
router.post(
  "/posts",
  validateRequest(createPostSchema),
  asyncHandler(async (req, res) => {
    const { title, content, published } = req.body;
    // Your logic here
    res
      .status(201)
      .json({ success: true, data: { id: 1, title, content, published } });
  }),
);

16 lines of documentation. Schema defined once in Zod. Used for both validation and documentation. Change the schema once, both update.


Protected endpoint with authentication

Any endpoint that requires a JWT token.

/**
 * @swagger
 * /api/posts/{id}:
 *   delete:
 *     summary: Delete a post
 *     tags:
 *       - Posts
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *         description: Post ID
 *     responses:
 *       204:
 *         description: Post deleted successfully
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 *       404:
 *         $ref: '#/components/responses/NotFound'
 */
router.delete(
  "/posts/:id",
  authenticateJWT,
  asyncHandler(async (req, res) => {
    const { id } = req.params;
    // Your logic here
    res.status(204).send();
  }),
);

The security section tells Swagger this endpoint requires authentication. The bearerAuth scheme is automatically configured by @charcoles/swagger. Swagger UI will show a lock icon and let users enter a JWT token for testing.


PATCH endpoint with partial updates

Updating resources with optional fields.

Step 1: Define schema

export const updatePostSchema = z.object({
  body: z.object({
    title: z.string().min(1).max(200).optional(),
    content: z.string().min(1).optional(),
    published: z.boolean().optional(),
  }),
});

Step 2: Register it

schemas: {
  updatePostSchema,
}

Step 3: Document it

/**
 * @swagger
 * /api/posts/{id}:
 *   patch:
 *     summary: Update a post
 *     tags:
 *       - Posts
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/updatePostSchema'
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 *       400:
 *         $ref: '#/components/responses/ValidationError'
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 *       404:
 *         $ref: '#/components/responses/NotFound'
 */
router.patch(
  "/posts/:id",
  authenticateJWT,
  validateRequest(updatePostSchema),
  asyncHandler(async (req, res) => {
    const { id } = req.params;
    const updates = req.body;
    // Your logic here
    res.json({ success: true, data: { id, ...updates } });
  }),
);

All optional fields in Zod automatically become optional in the OpenAPI schema. No manual conversion needed.


Complete CRUD example

A full set of CRUD operations for a resource.

// src/modules/items/items.schemas.ts
export const createItemSchema = z.object({
  body: z.object({
    name: z.string().min(1).max(100),
    description: z.string().optional(),
    price: z.number().positive(),
    category: z.enum(["electronics", "clothing", "books", "other"]),
  }),
});

export const updateItemSchema = z.object({
  body: z.object({
    name: z.string().min(1).max(100).optional(),
    description: z.string().optional(),
    price: z.number().positive().optional(),
    category: z.enum(["electronics", "clothing", "books", "other"]).optional(),
  }),
});
// src/config/swagger.config.ts
schemas: {
  createItemSchema,
  updateItemSchema,
}
// src/modules/items/items.routes.ts

/**
 * @swagger
 * /api/items:
 *   get:
 *     summary: Get all items
 *     tags:
 *       - Items
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 */
router.get(
  "/items",
  asyncHandler(async (req, res) => {
    // Your logic
    res.json({ success: true, data: [] });
  }),
);

/**
 * @swagger
 * /api/items/{id}:
 *   get:
 *     summary: Get item by ID
 *     tags:
 *       - Items
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 *       404:
 *         $ref: '#/components/responses/NotFound'
 */
router.get(
  "/items/:id",
  asyncHandler(async (req, res) => {
    // Your logic
    res.json({ success: true, data: {} });
  }),
);

/**
 * @swagger
 * /api/items:
 *   post:
 *     summary: Create a new item
 *     tags:
 *       - Items
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/createItemSchema'
 *     responses:
 *       201:
 *         $ref: '#/components/responses/Success'
 *       400:
 *         $ref: '#/components/responses/ValidationError'
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 */
router.post(
  "/items",
  authenticateJWT,
  validateRequest(createItemSchema),
  asyncHandler(async (req, res) => {
    // Your logic
    res.status(201).json({ success: true, data: req.body });
  }),
);

/**
 * @swagger
 * /api/items/{id}:
 *   put:
 *     summary: Update an item
 *     tags:
 *       - Items
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/updateItemSchema'
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 *       400:
 *         $ref: '#/components/responses/ValidationError'
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 *       404:
 *         $ref: '#/components/responses/NotFound'
 */
router.put(
  "/items/:id",
  authenticateJWT,
  validateRequest(updateItemSchema),
  asyncHandler(async (req, res) => {
    // Your logic
    res.json({ success: true, data: req.body });
  }),
);

/**
 * @swagger
 * /api/items/{id}:
 *   delete:
 *     summary: Delete an item
 *     tags:
 *       - Items
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *     responses:
 *       204:
 *         description: Item deleted successfully
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 *       404:
 *         $ref: '#/components/responses/NotFound'
 */
router.delete(
  "/items/:id",
  authenticateJWT,
  asyncHandler(async (req, res) => {
    // Your logic
    res.status(204).send();
  }),
);

Five endpoints. Two schemas. Complete CRUD API documented.


Authentication endpoints

The most critical endpoints in any API.

// Schemas already defined in auth.schemas.ts
export const registerSchema = z.object({
  body: z.object({
    name: z.string().min(1),
    email: z.string().email(),
    password: z.string().min(8),
  }),
});

export const loginSchema = z.object({
  body: z.object({
    email: z.string().email(),
    password: z.string().min(8),
  }),
});
// Registered in swagger.config.ts
schemas: {
  registerSchema,
  loginSchema,
}
// Documentation
/**
 * @swagger
 * /api/auth/register:
 *   post:
 *     summary: Register a new user
 *     tags:
 *       - Authentication
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/registerSchema'
 *     responses:
 *       201:
 *         $ref: '#/components/responses/Success'
 *       400:
 *         $ref: '#/components/responses/ValidationError'
 */
router.post(
  "/auth/register",
  validateRequest(registerSchema),
  AuthController.register,
);

/**
 * @swagger
 * /api/auth/login:
 *   post:
 *     summary: Login with email and password
 *     tags:
 *       - Authentication
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             $ref: '#/components/schemas/loginSchema'
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 *       400:
 *         $ref: '#/components/responses/ValidationError'
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 */
router.post("/auth/login", validateRequest(loginSchema), AuthController.login);

/**
 * @swagger
 * /api/auth/me:
 *   get:
 *     summary: Get current user profile
 *     tags:
 *       - Authentication
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 *       401:
 *         $ref: '#/components/responses/Unauthorized'
 */
router.get("/auth/me", authenticateJWT, AuthController.me);

Complete authentication flow documented with minimal effort.


Custom response schemas

For the 10% of cases where built-in responses are not enough.

// src/config/swagger.config.ts
const swaggerConfig = {
  schemas: {
    createUserSchema,
  },
  customResponses: {
    UserCreated: {
      description: "User created successfully with JWT token",
      content: {
        "application/json": {
          schema: {
            type: "object",
            properties: {
              success: { type: "boolean", example: true },
              token: {
                type: "string",
                example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
              },
              user: {
                type: "object",
                properties: {
                  id: { type: "string" },
                  email: { type: "string" },
                  name: { type: "string" },
                },
              },
            },
          },
        },
      },
    },
  },
};
/**
 * @swagger
 * /api/auth/register:
 *   post:
 *     responses:
 *       201:
 *         $ref: '#/components/responses/UserCreated'
 */

Define once, reuse everywhere.


Query parameters example

For endpoints with filters, pagination, or search.

/**
 * @swagger
 * /api/posts:
 *   get:
 *     summary: Get all posts with optional filters
 *     tags:
 *       - Posts
 *     parameters:
 *       - in: query
 *         name: published
 *         schema:
 *           type: boolean
 *         description: Filter by published status
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *           default: 10
 *         description: Number of posts to return
 *       - in: query
 *         name: offset
 *         schema:
 *           type: integer
 *           default: 0
 *         description: Number of posts to skip
 *     responses:
 *       200:
 *         $ref: '#/components/responses/Success'
 */
router.get(
  "/posts",
  asyncHandler(async (req, res) => {
    const { published, limit = 10, offset = 0 } = req.query;
    // Your logic
    res.json({ success: true, data: [] });
  }),
);

Query parameters are still manual because they are not part of the request body schema. Zod can validate query params, but OpenAPI requires them separately in the parameters section.


Key patterns

All these examples follow the same pattern:

  1. Define schema once in Zod (for validation)
  2. Register schema once in Swagger config (auto-converts to OpenAPI)
  3. Reference schema in documentation with $ref
  4. Use response templates for common cases

The result is clean, minimal, and impossible to get out of sync.

Change a validation rule? Documentation updates automatically. Add a new field? Swagger reflects it immediately. No duplication. No drift. No maintenance burden.

That is the entire point of @charcoles/swagger.