Docus Logo
Charcole Swagger

Swagger for Non-Charcole Projects

How to use @charcoles/swagger in any Express.js project, even if you didn't use create-charcole.

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/swagger for auto-generated documentation
  • zod for 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:

  1. Check the examples documentation
  2. Look at the main Swagger guide
  3. 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.