Swagger Examples
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:
- Define schema once in Zod (for validation)
- Register schema once in Swagger config (auto-converts to OpenAPI)
- Reference schema in documentation with
$ref - 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.