npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

shapeguard

v0.3.0

Published

FastAPI-style validation, response shaping, and error handling for Node.js + Express

Downloads

243

Readme

shapeguard

FastAPI-style validation, response shaping, and error handling for Node.js + Express.

Zero config to start. Fully configurable when you need it. Strict by default. Lightweight. Production-ready.

npm bundle size license node CI


Why shapeguard

shapeguard vs manual setup

Every Node.js + Express app repeats the same setup:

  • Manual if (!req.body.email) checks scattered everywhere
  • Error responses with different shapes per developer
  • passwordHash leaking because someone forgot to strip it
  • req.body typed as any — no IDE help, no safety
  • Unhandled promise rejections silently hanging requests in Express 4
  • DB error messages exposed to clients in production

shapeguard fixes all of this permanently — once, at setup.


Install

npm install shapeguard zod
# Optional — structured production logging
npm install pino pino-pretty

If pino is not installed, shapeguard uses a built-in console logger automatically.


Peer dependencies

| Package | Required | Notes | |---------------|----------|--------------------------------| | express | Yes | Primary target | | zod | Yes | Schema validation | | pino | Optional | Richer logging if installed | | pino-pretty | Optional | Pretty dev logs with pino | | joi | Optional | Via shapeguard/adapters/joi | | yup | Optional | Via shapeguard/adapters/yup |


Quick start

1. Mount in app.ts

import express from 'express'
import { shapeguard, notFoundHandler, errorHandler } from 'shapeguard'

const app = express()
app.use(express.json())
app.use(shapeguard())         // logging, requestId, res helpers
app.use('/api/users', userRouter)
app.use(notFoundHandler())    // 404 — no route matched
app.use(errorHandler())       // catches everything — always last
app.listen(3000)

2. Define schemas once

// validators/user.validator.ts
import { z } from 'zod'
import { defineRoute, createDTO } from 'shapeguard'

// createDTO() infers the TypeScript type automatically — no manual z.infer needed
const CreateUserDTO = createDTO({
  email:    z.string().email(),
  name:     z.string().min(1).max(100),
  password: z.string().min(8),
})

const UserResponseSchema = z.object({
  id:        z.string().uuid(),
  email:     z.string(),
  name:      z.string(),
  createdAt: z.string().datetime(),
  // passwordHash NOT here → stripped automatically
})

export const CreateUserRoute = defineRoute({
  body:      CreateUserDTO,
  response:  UserResponseSchema,
  // optional: transform runs after validation, before your handler
  transform: async (data) => ({
    ...data,
    password: await bcrypt.hash(data.password, 10),
  }),
})

export type CreateUserBody = CreateUserDTO.Input  // ← no z.infer needed
export type UserResponse   = z.infer<typeof UserResponseSchema>

3. Validate and handle in one step

import { handle, AppError } from 'shapeguard'
import { CreateUserRoute } from '../validators/user.validator'

export const createUser = handle(CreateUserRoute, async (req, res) => {
  req.body.email   // string — typed ✅
  req.body.isAdmin // TypeScript error — not in schema ✅

  const user = await UserService.create(req.body)
  res.created({ data: user, message: 'User created' })
  // passwordHash stripped by response schema automatically ✅
})

handle() combines validate() + asyncHandler() into one call. The two-element array pattern from v0.1.x still works — migrate at your own pace.


### 4. Throw errors from anywhere

```ts
import { AppError } from 'shapeguard'

async create(data: CreateUserBody) {
  const exists = await db.findByEmail(data.email)
  if (exists) throw AppError.conflict('Email')
  // → client sees "Email already exists" — 409

  return db.create(data)
  // crash → client sees "Something went wrong" in prod
  // full stack trace → pino logger only ✅
}

5. Routes with auto 405

import { createRouter } from 'shapeguard'

const router = createRouter()  // drop-in for express.Router()

router.post('/',      ...UserController.createUser)
router.get('/',       ...UserController.listUsers)
// DELETE / → 405 Method Not Allowed, Allow: GET, POST — automatic

router.get('/:id',    ...UserController.getUser)
router.put('/:id',    ...UserController.updateUser)
router.delete('/:id', ...UserController.deleteUser)
// POST /:id → 405, Allow: GET, PUT, DELETE — works for :param routes too

export default router

What you get for free

shapeguard() mounted
  ✅ Structured logging          dev: pretty  |  prod: JSON lines
  ✅ Time-ordered requestId      every request, traceable in logs
  ✅ Request logging             method, endpoint (not path), status, duration
  ✅ res helpers                 res.ok / res.created / res.fail on every route
  ✅ Default redaction           passwords, tokens, cookies never logged

validate() on a route
  ✅ req.body typed              no more any
  ✅ Unknown fields stripped     silently — passwordHash gone before service runs
  ✅ Fail fast                   first invalid field → 422 → handler never runs
  ✅ Response stripping          response schema removes server-only fields
  ✅ Pre-parse guards            proto pollution, depth DoS, size limits

AppError thrown anywhere
  ✅ errorHandler catches it     always consistent response shape
  ✅ Operational → shown         client sees your message
  ✅ Programmer → hidden in prod "Something went wrong"

errorHandler() mounted
  ✅ One error shape always      frontend writes one handler — forever
  ✅ 4xx → logger.warn           expected, low noise
  ✅ 5xx → logger.error + stack  unexpected, full detail
  ✅ onError hook                Sentry, Datadog, alerting

Response shapes — always consistent

// SUCCESS
{ "success": true,  "message": "User created", "data": { ... } }

// ERROR
{ "success": false, "message": "Validation failed",
  "error": { "code": "VALIDATION_ERROR", "message": "...",
             "details": { "field": "email", "message": "Invalid email" }}}

Frontend writes this once, forever:

const { success, data, error } = response.data
if (!success) handleError(error.code, error.message)

Logging

# Development — human readable, color-coded level badges, one line per request
09:44:57.123  [DEBUG]  >>  POST    /api/v1/users                       [req_019c...]
09:44:57.125  [INFO]   <<  201  POST    /api/v1/users           2ms   [req_019c...]
09:44:57.400  [WARN]   <<  404  GET     /api/v1/users/xx       12ms   [req_019c...]
09:44:57.900  [ERROR]  <<  500  GET     /api/v1/crash           1ms   [req_019c...]
09:44:57.800  [WARN]   <<  200  GET     /api/v1/data         1523ms   [req_019c...]  SLOW

# Production — one JSON line per event (Datadog / CloudWatch / Loki ready)
{"level":"info","time":"...","requestId":"req_019c...","method":"POST","endpoint":"/api/v1/users","status":201,"duration_ms":2}

>> = request arriving at server  |  << = response leaving server

Request ID config

app.use(shapeguard({
  requestId: {
    // Read trace ID from upstream first (load balancer / API gateway / CDN).
    // Falls back to generating a fresh req_<ts><random> ID if header is absent.
    header: 'x-request-id',        // default. Change to 'x-trace-id', 'x-correlation-id', etc.

    // Custom generator — replace built-in format
    // generator: () => `trace-${crypto.randomUUID()}`,

    // Disable entirely — req.id will be '' and no ID appears in logs
    // enabled: false,
  },
  logger: {
    logRequestId:   true,           // show [req_id] on every log line (default: true)
    logAllRequests: true,           // log every request, not just errors (default: true in dev)
    slowThreshold:  1000,           // SLOW warning if response >= 1000ms
  },
  response: {
    includeRequestId: true,         // send X-Request-Id header on every response
  },
}))

Optional body logging

app.use(shapeguard({
  logger: {
    logRequestBody:  true,   // include req.body in log (auto-redacted: passwords/tokens never logged)
    logResponseBody: true,   // include response JSON in log
  }
}))

Full API

Tier 1 — daily use

import {
  handle,            // validate + asyncHandler in one — recommended from v0.2.0
  validate,          // request validation + response stripping
  asyncHandler,      // async route safety for Express 4
  AppError,          // throw errors from anywhere
  defineRoute,       // bundle schemas — single source of truth
  createDTO,         // z.object() wrapper with auto type inference
  isAppError,        // type guard
  ErrorCode,         // stable error code constants
} from 'shapeguard'

Tier 2 — setup once

import {
  shapeguard,        // main middleware — mount in app.ts
  errorHandler,      // centralised error handler — always last
  notFoundHandler,   // 404 for unmatched routes
  createRouter,      // drop-in router with auto 405
} from 'shapeguard'

Tier 3 — special cases

import {
  withShape,         // custom response shape per route
  zodAdapter,        // wrap zod schemas manually
  isZodSchema,       // detect zod schemas
} from 'shapeguard'

Types

import type {
  ShapeguardConfig,   // shapeguard() config
  LoggerConfig,       // logger sub-config
  ValidationConfig,   // validation sub-config
  ResponseConfig,     // response sub-config
  ErrorsConfig,       // errors sub-config
  SchemaAdapter,      // adapter interface for custom schemas
  RouteSchema,        // defineRoute() shape
  SuccessEnvelope,    // { success: true, message, data }
  ErrorEnvelope,      // { success: false, message, error }
  Envelope,           // union of both
  PaginatedData,      // paginated data shape
  ValidationIssue,    // { field, message, code }
  Logger,             // { info, warn, error, debug }
  LogLevel,           // 'debug' | 'info' | 'warn' | 'error'

  // Type inference from defineRoute() output
  InferBody,          // type Body = InferBody<typeof MyRoute>
  InferParams,        // type Params = InferParams<typeof MyRoute>
  InferQuery,         // type Query = InferQuery<typeof MyRoute>
  InferHeaders,       // type Headers = InferHeaders<typeof MyRoute>
} from 'shapeguard'

Adapters

// Joi
import { joiAdapter } from 'shapeguard/adapters/joi'
// Yup
import { yupAdapter } from 'shapeguard/adapters/yup'

Security defaults

| Threat | Default | Configurable | |--------|---------|--------------| | Proto pollution (__proto__) | Stripped | No — always blocked | | Unicode injection (null bytes, RTL) | Stripped | No — always cleaned | | Object depth DoS | 20 levels | Yes — per-route or global | | Array size DoS | 1000 items | Yes | | String size DoS | 10,000 chars | Yes | | Missing Content-Type | 415 error | No — always enforced | | Sensitive fields in logs | [REDACTED] | Add more paths | | Programmer errors in prod | Hidden | fallbackMessage to customise |


Bundle size

shapeguard core   ~12kb gzip   — zero heavy deps, tree-shakeable
pino (optional)   ~8kb  gzip   — adds structured JSON logging
total             ~20kb gzip

Full documentation

| Doc | What's inside | |-----|---------------| | VALIDATION.md | validate(), handle(), defineRoute(), createDTO(), transform hook, adapters | | ERRORS.md | AppError, errorHandler, operational vs programmer | | LOGGING.md | pino, requestId, body logging, dev vs prod, config | | RESPONSE.md | res helpers, withShape, all response shapes | | CONFIGURATION.md | every config option, global vs scoped | | MIGRATION.md | upgrade guide — v0.1.x → v0.2.0 | | CHANGELOG.md | version history | | SETUP.md | GitHub + npm publish steps |

Examples

Examples live on GitHub — not included in the npm package.

| Example | What it shows | |---------|---------------| | basic-crud-api | Full CRUD app — all features working together | | handle-and-dto | handle() + createDTO() — less boilerplate | | transform-hook | Password hashing, slug generation via transform | | global-config | validation.strings, logger.silent, custom request ID |


License

MIT © 2026 Kalyan Kashaboina