Express → Postman collection
Express has no route table you can export and no built-in schema, so most guides start with swagger-jsdoc and a page of YAML comments. routeman-cli reads your routers and handlers directly and writes a full Postman collection in one command, without starting the server.
What routeman-cli reads from an Express app
- Routes:
express.Router(),app.use('/prefix', router)mounts (nested as deep as you like),router.route('/x').get().post(), sub-apps, router factories, and routes registered in loops or loaded withfs.readdirSync. Express 4 and Express 5 path syntax, including*splat. - Bodies from schemas: zod, Joi and celebrate, yup, valibot, express-validator chains and TypeScript types such as
Request<{}, {}, CreateUserBody>. - Bodies from the handler code:
const { email, password } = req.body,req.body.name, with types inferred from casts likeNumber(...). These requests are marked as inferred in their description. - Uploads: multer's
upload.single('image')becomes amultipart/form-databody with a Postman file picker. - Query parameters:
req.query.statusand destructuredconst { page = 1, search } = req.query. - Auth: middleware that calls
jwt.verify,passport.authenticate('jwt')or reads theAuthorizationheader. Routes that don't pass through it get No Auth. - The port:
app.listen(4000)orPORTfrom.envsets thelocalenvironment'sbase_url.
Example
A small shop API: three routers mounted under /api/v1, a JWT middleware, zod schemas on some routes and plain req.body on others.
// src/server.js
const app = express();
app.use(express.json());
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/products', productsRouter);
app.use('/api/v1/orders', ordersRouter);
app.listen(process.env.PORT || 3000); // .env has PORT=4000
// src/middleware/auth.js
export function requireAuth(req, res, next) {
const token = (req.headers.authorization || '').replace('Bearer ', '');
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
}
// src/routes/auth.js
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await findUser(email, password);
res.json({ access_token: jwt.sign({ sub: user.id }, process.env.JWT_SECRET) });
});
// 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.get('/', async (req, res) => {
const { page = 1, search, category } = req.query;
...
});
router.post('/', requireAuth, async (req, res) => {
const data = productSchema.parse(req.body);
...
});
router.post('/:productId/image', requireAuth, upload.single('image'), ...);
// src/routes/orders.js
router.use(requireAuth);
router.post('/', async (req, res) => {
const { productId, quantity, couponCode } = req.body;
...
});
$ 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)
$ 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)
This is real output from routeman-cli 0.1.0. In Postman:
- Auth › Login sends
{"email": "{{username}}", "password": "{{password}}"}and its test script savesaccess_tokeninto the environment. Login and Register are No Auth. - Products › Create product gets a body that passes the zod schema:
{"name": "John Doe", "price": 100, "sku": "AAA-1111", "category": "books", "stock": 0}. The SKU matches the regex, and the category is one of the enum values. - Products › List products has
page,searchandcategoryquery parameters, switched off until you need them. It's public, because the route has norequireAuth. - Products › Create image is a
form-datarequest with animagefile picker, found from multer'supload.single('image'). - Orders are all authenticated, because of
router.use(requireAuth). The Create order body was read from the destructuredreq.body, so its description notes that the field list was inferred. - The
localenvironment'sbase_urlishttp://localhost:4000, taken fromPORTin.env.{{productId}}and{{orderId}}are environment variables.
Request names and descriptions
Requests are named from the method and the resource: List products, Get product, Create product. If your handlers are named functions with a JSDoc comment, the comment's first line becomes the request name:
/**
* List all widgets
*/
function listWidgets(req, res) { ... }
app.get('/widgets', listWidgets); // → "List all widgets"
Every description holds a table of body and query fields (type, required, notes) and the source location, such as src/routes/orders.js:8, so you can jump from Postman back to the handler.
TypeScript, monorepos and custom entry files
TypeScript is parsed directly, so you don't need a build step. routeman-cli finds the entry file from your package.json main field and scripts. It resolves tsconfig paths and package.json imports too. If it starts from the wrong file, pass --entry src/server.ts (repeatable), or point at a workspace package with -C apps/api.
If a route is missing, run npx routeman-cli routes and read the warnings. Usually the cause is a path built at runtime from data routeman-cli can't see, such as a value from the database.
Already have swagger-jsdoc?
You can keep it. To build the collection from the spec instead of the code, run npx routeman-cli --openapi ./openapi.json (or a URL). You still get the login script, environments and smoke tests. Most teams get a fuller result from the code, though, because hand-written JSDoc YAML tends to fall behind the handlers.
Full walkthrough: Express API to Postman collection without Swagger.