ExpressNode.jsPostmanTutorial

Express API to Postman collection without Swagger

Export every route of an Express app to a Postman collection, with zod bodies, multer uploads and JWT login, without swagger-jsdoc or a running server. Real output included.

Express is the most popular Node.js framework, and it has no route table you can export. app._router.stack is private and changed in Express 5. swagger-jsdoc means writing a YAML block above every route, and route printers like express-list-endpoints give you paths, not requests.

routeman-cli reads your routers the way Express would mount them, and writes a Postman collection with bodies, auth and environments. Here it is on a small shop API.

The app

Three routers mounted under /api/v1, a JWT middleware, zod on the product routes and plain destructuring on the order routes:

// src/server.js
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/products', productsRouter);
app.use('/api/v1/orders', ordersRouter);
app.get('/health', (req, res) => res.json({ ok: true }));
app.listen(process.env.PORT || 3000);

// src/routes/products.js
const productSchema = z.object({
  name: z.string().min(1).max(120),
  price: z.number().positive(),
  sku: z.string().regex(/^[A-Z]{3}-\d{4}$/),
  category: z.enum(['books', 'electronics', 'clothing']),
  stock: z.number().int().min(0).default(0),
});

router.post('/', requireAuth, async (req, res) => {
  const data = productSchema.parse(req.body);
  res.status(201).json(await createProduct(data));
});
router.post('/:productId/image', requireAuth, upload.single('image'), uploadImage);

// src/routes/orders.js
router.use(requireAuth);
router.post('/', async (req, res) => {
  const { productId, quantity, couponCode } = req.body;
  res.status(201).json(await placeOrder(req.user.sub, productId, Number(quantity), couponCode));
});

requireAuth calls jwt.verify on the Bearer token, and .env sets PORT=4000.

See what routeman-cli finds

Start with routes. It's a dry run that changes nothing:

$ npx routeman-cli routes
POST    /api/v1/auth/login                    json: email, password
POST    /api/v1/auth/register                 json: email, password, name
GET     /api/v1/orders                      πŸ”’
POST    /api/v1/orders                      πŸ”’ json: productId, quantity, couponCode
PATCH   /api/v1/orders/{orderId}/cancel     πŸ”’
GET     /api/v1/products
POST    /api/v1/products                    πŸ”’ json: name, price, sku, category, stock
DELETE  /api/v1/products/{productId}        πŸ”’
GET     /api/v1/products/{productId}
PUT     /api/v1/products/{productId}        πŸ”’ json: name, price, sku, category, stock
POST    /api/v1/products/{productId}/image  πŸ”’ form: image
GET     /health
12 routes, auth: bearer (login: POST /api/v1/auth/login)

Every mount prefix is applied. Auth follows the middleware exactly: requireAuth on single product routes, and router.use(requireAuth) on every order route. The multer upload is a form field.

Generate

$ npx routeman-cli
βœ“ express: 12 requests (5 POST, 4 GET, 1 PUT, 1 DELETE, 1 PATCH)
βœ“ auth: bearer (login: POST /api/v1/auth/login)
βœ“ wrote postman/shop-api.postman_collection.json
βœ“ wrote postman/shop-api.local.postman_environment.json
  done in 0.08s - import the files in Postman (File β†’ Import)

Here is the Create product request as it appears in the collection. The values satisfy the zod schema, including the regex and the enum:

{
  "name": "John Doe",
  "price": 100,
  "sku": "AAA-1111",
  "category": "books",
  "stock": 0
}

Its description holds a field table generated from the schema. category lists the three allowed values, stock is optional because of its default, and the source location is src/routes/products.js:29.

The Create order body, {"productId": "string", "quantity": 1, "couponCode": "123456"}, was read from the destructuring in the handler. The Number(quantity) cast made quantity a number. Its description starts with a note that the fields were inferred from the code. Add a zod schema to that route, and the next run will use it instead.

Log in once, then send everything

Select the local environment (base_url is http://localhost:4000, from .env), fill in username and password, and send Auth β€Ί Login. Its test script, generated by routeman-cli, finds the token wherever it is in the response:

const access = find(body, ['access_token', 'accessToken', 'access', 'token', 'jwt', 'id_token', 'idToken', 'key', 'auth_token', 'authToken'], 0);
const refresh = find(body, ['refresh_token', 'refreshToken', 'refresh'], 0);
if (access) { pm.environment.set('access_token', access); pm.collectionVariables.set('access_token', access); }

Every protected request sends Bearer {{access_token}} from then on. {{productId}} and {{orderId}} live in the environment, so set them once from a list response.

Keep it in sync

Add a script to package.json and run it whenever routes change. Collection and request ids are stable, so re-importing replaces the collection in Postman instead of duplicating it:

{
  "scripts": {
    "postman": "routeman -e staging=https://staging.example.com"
  },
  "devDependencies": {
    "routeman-cli": "^0.1.0"
  }
}

For TypeScript projects nothing changes. routeman-cli parses .ts directly and resolves tsconfig paths. If the entry file isn't in package.json's main or scripts, add --entry src/server.ts.

Try routeman on your project

$ cd your-api
$ npx routeman-cli

Read the docs β†’ Ask a question