Swagger for Non-Charcole Projects
Using @charcoles/swagger without Charcole
One of the design goals of @charcoles/swagger is that it works everywhere.
It is not locked to Charcole projects. It does not require any Charcole CLI. It does not depend on Charcole architecture.
If you have an Express.js project and you use Zod for validation, you can install @charcoles/swagger and immediately eliminate schema duplication.
Even if you have never heard of Charcole before.
Installation
npm install @charcoles/swagger zod
That's it.
Two dependencies:
@charcoles/swaggerfor auto-generated documentationzodfor schema validation (you probably already have this)
No configuration files required. No build steps. No additional setup.
Basic setup
In your main Express file (usually app.js or server.js or index.js):
import express from "express";
import { setupSwagger } from "@charcoles/swagger";
const app = express();
app.use(express.json());
// Setup Swagger
setupSwagger(app, {
title: "My API",
version: "1.0.0",
description: "My awesome API documentation",
});
// Your routes here
app.use("/api/users", userRoutes);
app.use("/api/posts", postRoutes);
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
console.log("Swagger docs available at http://localhost:3000/api-docs");
});
Start your server.
Visit http://localhost:3000/api-docs
Swagger UI is live.
Adding your Zod schemas
If you already use Zod for validation, you already have schemas.
Before @charcoles/swagger:
// schemas/user.js
import { z } from "zod";
export const createUserSchema = z.object({
body: z.object({
name: z.string().min(1),
email: z.string().email(),
password: z.string().min(8),
}),
});
// routes/users.js
router.post("/users", async (req, res) => {
// Validate manually
const result = createUserSchema.safeParse({ body: req.body });
if (!result.success) {
return res.status(400).json({ errors: result.error });
}
// Your logic here
});
After @charcoles/swagger:
Just register the schema:
// app.js
import { createUserSchema } from "./schemas/user.js";
setupSwagger(app, {
title: "My API",
version: "1.0.0",
schemas: {
createUserSchema, // Auto-converted to OpenAPI!
},
});
Now in your routes, add JSDoc comments:
/**
* @swagger
* /api/users:
* post:
* summary: Create a new user
* tags:
* - Users
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/createUserSchema'
* responses:
* 201:
* $ref: '#/components/responses/Success'
* 400:
* $ref: '#/components/responses/ValidationError'
*/
router.post("/users", async (req, res) => {
const result = createUserSchema.safeParse({ body: req.body });
if (!result.success) {
return res.status(400).json({ errors: result.error });
}
// Your logic
});
Done.
Schema defined once in Zod. Documentation generated automatically. No duplication.
Integration with existing validation middleware
If you already have validation middleware, nothing changes.
Example with express-validator style middleware:
// middleware/validate.js
export function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse({
body: req.body,
params: req.params,
query: req.query,
});
if (!result.success) {
return res.status(400).json({ errors: result.error.errors });
}
next();
};
}
// routes/users.js
/**
* @swagger
* /api/users:
* post:
* requestBody:
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/createUserSchema'
* responses:
* 201:
* $ref: '#/components/responses/Success'
*/
router.post("/users", validate(createUserSchema), async (req, res) => {
// Logic here - validation already happened
});
Your existing middleware continues working.
You just add the JSDoc comment with $ref.
Swagger documentation is auto-generated from the same schema your middleware uses.
Works with CommonJS or ES Modules
CommonJS (require):
const express = require("express");
const { setupSwagger } = require("@charcoles/swagger");
const { z } = require("zod");
const app = express();
setupSwagger(app, {
title: "My API",
version: "1.0.0",
});
module.exports = app;
ES Modules (import):
import express from "express";
import { setupSwagger } from "@charcoles/swagger";
import { z } from "zod";
const app = express();
setupSwagger(app, {
title: "My API",
version: "1.0.0",
});
export default app;
Both work identically.
Works with JavaScript or TypeScript
JavaScript:
// schemas/post.js
import { z } from "zod";
export const createPostSchema = z.object({
body: z.object({
title: z.string(),
content: z.string(),
}),
});
TypeScript:
// schemas/post.ts
import { z } from "zod";
export const createPostSchema = z.object({
body: z.object({
title: z.string(),
content: z.string(),
}),
});
// Type inference works!
type CreatePostInput = z.infer<typeof createPostSchema>;
Same setup. Same registration. Same documentation.
Configuration options
Full list of options you can pass to setupSwagger():
setupSwagger(app, {
// Basic info
title: "My API", // API title
version: "1.0.0", // API version
description: "API documentation", // API description
// Paths
path: "/api-docs", // Swagger UI path (default: /api-docs)
// Servers
servers: [
{ url: "http://localhost:3000", description: "Development" },
{ url: "https://api.example.com", description: "Production" },
],
// Auto-register schemas
schemas: {
createUserSchema,
updateUserSchema,
loginSchema,
// ... all your Zod schemas
},
// Built-in responses (enabled by default)
includeCommonResponses: true, // Success, ValidationError, Unauthorized, etc.
// Custom responses
customResponses: {
UserCreated: {
description: "User created with token",
content: {
"application/json": {
schema: {
type: "object",
properties: {
token: { type: "string" },
user: { type: "object" },
},
},
},
},
},
},
});
Real project example
Here's a complete minimal project structure:
my-api/
├── app.js
├── routes/
│ ├── users.js
│ └── posts.js
├── schemas/
│ ├── user.js
│ └── post.js
└── package.json
package.json:
{
"type": "module",
"dependencies": {
"express": "^4.18.2",
"zod": "^3.25.0",
"@charcoles/swagger": "^2.0.0"
}
}
schemas/user.js:
import { z } from "zod";
export const createUserSchema = z.object({
body: z.object({
name: z.string().min(1),
email: z.string().email(),
}),
});
app.js:
import express from "express";
import { setupSwagger } from "@charcoles/swagger";
import { createUserSchema } from "./schemas/user.js";
import userRoutes from "./routes/users.js";
const app = express();
app.use(express.json());
setupSwagger(app, {
title: "My API",
version: "1.0.0",
schemas: {
createUserSchema,
},
});
app.use("/api/users", userRoutes);
app.listen(3000);
routes/users.js:
import express from "express";
import { createUserSchema } from "../schemas/user.js";
const router = express.Router();
/**
* @swagger
* /api/users:
* post:
* summary: Create user
* requestBody:
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/createUserSchema'
* responses:
* 201:
* $ref: '#/components/responses/Success'
* 400:
* $ref: '#/components/responses/ValidationError'
*/
router.post("/", (req, res) => {
const result = createUserSchema.safeParse({ body: req.body });
if (!result.success) {
return res.status(400).json({ errors: result.error.errors });
}
res.status(201).json({ success: true, data: req.body });
});
export default router;
Run npm start
Visit http://localhost:3000/api-docs
Working Swagger documentation with zero duplication.
Migrating from swagger-jsdoc
If you currently use swagger-jsdoc, the migration is straightforward.
Before:
import swaggerJsdoc from "swagger-jsdoc";
import swaggerUi from "swagger-ui-express";
const specs = swaggerJsdoc({
definition: {
openapi: "3.0.0",
info: {
title: "My API",
version: "1.0.0",
},
},
apis: ["./routes/*.js"],
});
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));
After:
import { setupSwagger } from "@charcoles/swagger";
setupSwagger(app, {
title: "My API",
version: "1.0.0",
schemas: {
// Your Zod schemas here
},
});
Your existing JSDoc comments still work.
You just gain the ability to use $ref to auto-generated schemas.
When to use @charcoles/swagger
Use it if:
- You have an Express.js API
- You use Zod for validation
- You want to eliminate schema duplication
- You want Swagger documentation that stays accurate
Do not use it if:
- You do not use Zod (stick with manual Swagger)
- You do not use Express (it is Express-specific)
- You prefer code-first approaches like NestJS or tRPC
Getting help
If something is unclear:
- Check the examples documentation
- Look at the main Swagger guide
- Read the package README:
node_modules/@charcoles/swagger/README.md
The package is simple by design. There are not many moving parts.
Final thoughts
@charcoles/swagger is a standalone package.
It does not care how your project is structured. It does not care if you use Charcole or not. It does not care if you use TypeScript or JavaScript.
It solves one specific problem: converting Zod schemas to OpenAPI documentation.
If that is a problem you have, the package will solve it in about five minutes.