Before and after
The same user creation endpoint โ without and with shapeguard.
// install 9 packages: express-validator, http-errors,
// morgan, express-rate-limit, express-async-errors,
// swagger-ui-express, swagger-jsdoc, uuid, supertest
// โ manual validation โ different shape per developer
if (!req.body.email) return res.status(400).json({ error: 'email required' })
if (!req.body.name) return res.status(400).json({ msg: 'Name missing' })
// โ passwordHash ships to clients silently
res.json(user)
// โ unhandled async โ request hangs in Express 4
app.get('/users/:id', async (req, res) => {
const user = await db.find(req.params.id) // throws?
res.json(user)
})
// npm install shapeguard zod
// โ
One setup โ validate, log, strip, catch errors
app.use(shapeguard())
app.use(errorHandler()) // catches everything, always consistent
const CreateUserRoute = defineRoute({
body: createDTO(z.object({
email: z.string().email(),
name: z.string().min(1),
})),
response: z.object({ id: z.string(), email: z.string() }),
// password NOT listed โ auto-stripped before response
})
router.post('/users', ...handle(CreateUserRoute, async (req, res) => {
// req.body typed. async errors caught. sensitive fields stripped.
const user = await UserService.create(req.body)
res.created({ data: user, message: 'User created' })
}))
What it replaces
Every package below is one you no longer need to install, configure, or maintain.
| Package removed | shapeguard equivalent |
|---|---|
| express-validator | defineRoute() + handle() |
| express-async-errors | built into handle() + asyncHandler() |
| http-errors | AppError with typed factories |
| morgan | shapeguard() built-in structured logging |
| express-rate-limit | defineRoute({ rateLimit }) |
| swagger-ui-express | serveScalar() / serveSwaggerUI() / serveRedoc() โ CDN |
| swagger-jsdoc | generateOpenAPI() โ from route definitions |
| uuid | requestId built into shapeguard() |
| supertest mocks | mockRequest / mockResponse / mockNext |
| express-healthcheck | healthCheck() โ parallel checks, k8s-ready |
| http-graceful-shutdown | gracefulShutdown() โ drain + cleanup + exit |
9 packages โ 1 ย ยทย health check + graceful shutdown included ย ยทย ~18KB gzipped
What you get
Every feature is production-tested, security-hardened, and works independently.
Typed validation โ all errors at once
Zod schemas validate body, params, query, and headers. All failures returned in one response โ never one at a time.
Automatic response stripping
Define the response schema. passwordHash, token, and any unlisted field are removed before the response is sent.
Consistent error shape โ always
Every error โ validation, 404, thrown AppError, uncaught crash โ produces the exact same JSON structure.
Structured logging โ zero config
Auto-selects pino โ winston โ built-in fallback. Dev: color-coded. Prod: JSON for Datadog / CloudWatch / Loki.
OpenAPI + three UI choices
generateOpenAPI() reads your route definitions. Serve Scalar, Swagger UI, or Redoc โ all CDN-loaded, zero install.
Export to Postman, Insomnia, Bruno
toPostman(), toInsomnia(), toBruno() โ export your full API spec to any HTTP client with a single function call.
Webhook verification
Stripe, GitHub, Shopify, Svix, Twilio, or custom HMAC โ one line, timingSafeEqual, replay attack protection.
Rate limiting โ no extra package
Per-route rate limiting with a synchronous in-memory store. Pass a Redis store for distributed deployments.
Health check โ k8s-ready
healthCheck() runs all checks in parallel with independent timeouts. Returns 200/503 โ correct for liveness and readiness probes.
Graceful shutdown โ production-safe
gracefulShutdown() handles SIGTERM and SIGINT: drains in-flight requests, runs cleanup hooks, then exits cleanly.
Per-route request timeout
defineRoute({ timeout: 5000 }) aborts handlers that exceed the limit with a 408 โ no more hanging requests.
Test helpers โ no HTTP server
mockRequest(), mockResponse(), mockNext() โ test controllers in pure Node, no Express app, no ports.
Every feature is standalone
Use only what you need. Add logging today, validation tomorrow. Nothing forces you to adopt everything at once.
Full setup in one file
Mount shapeguard once. Every route gets validation, logging, error handling, and response stripping automatically.
import express from 'express'
import { z } from 'zod'
import {
shapeguard, defineRoute, handle,
createDTO, AppError, errorHandler, notFoundHandler,
} from 'shapeguard'
const app = express()
app.use(express.json())
app.use(shapeguard()) // logging + requestId + security guards
app.use(notFoundHandler()) // 404 for unmatched routes
app.use(errorHandler()) // catches everything thrown anywhere
const CreateUserRoute = defineRoute({
body: createDTO(z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
password: z.string().min(8),
})),
response: z.object({ id: z.string(), email: z.string(), name: z.string() }),
// password NOT in response โ automatically stripped
})
app.post('/users', ...handle(CreateUserRoute, async (req, res) => {
const user = await UserService.create(req.body) // typed, async errors caught
res.created({ data: user, message: 'User created' })
}))
app.listen(3000)
// โ validation, logging, errors, response stripping โ all working