Skip to main content
๐Ÿš€v0.10.0 โ€” zero runtime dependencies

Stop writing
Express boilerplate.

shapeguard replaces 9 packages with 1 โ€” typed validation, structured logging, consistent errors, and API docs for Express. Adopt one feature at a time. Nothing is mandatory.

npm versionbundle sizedownloadsCI
npm install shapeguard zod
9 โ†’ 1Packages replaced
~18KBGzipped (main)
0Runtime deps
800+Tests passing
ESM+CJSModule formats

Before and after

The same user creation endpoint โ€” without and with shapeguard.

โœ• Without 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)
})
โœ“ With shapeguard
// 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 removedshapeguard equivalent
express-validatordefineRoute() + handle()
express-async-errorsbuilt into handle() + asyncHandler()
http-errorsAppError with typed factories
morganshapeguard() built-in structured logging
express-rate-limitdefineRoute({ rateLimit })
swagger-ui-expressserveScalar() / serveSwaggerUI() / serveRedoc() โ€” CDN
swagger-jsdocgenerateOpenAPI() โ€” from route definitions
uuidrequestId built into shapeguard()
supertest mocksmockRequest / mockResponse / mockNext
express-healthcheckhealthCheck() โ€” parallel checks, k8s-ready
http-graceful-shutdowngracefulShutdown() โ€” 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.

app.ts
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