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

zodify-express

v0.1.4

Published

Type-safe Express route definitions with Zod validation. Infer req.body, req.query, req.params, and custom properties like req.user automatically.

Readme

Validate once. Infer everywhere. Define your Express route handlers with Zod schemas and get fully typed req.body, req.query, req.params — plus any custom properties like req.user — with zero manual type annotations.


✨ Features

  • 🔒 Zod-powered validation — body, query, and params parsed & validated at runtime
  • 🧠 Full type inference — no manual generics, no as casts, no any
  • 👤 Custom request properties — type req.user, req.session, or anything your middleware adds
  • 🎛️ Customizable error handling — plug in your own validation error response
  • 📦 Tiny footprint — ~3 kB, zero runtime dependencies
  • 🔀 Dual CJS/ESM — works everywhere, ships with .d.ts types

📦 Install

npm install zodify-express

Peer dependencies: You also need express and zod in your project.

npm install express zod

🚀 Quick Start

import express from "express";
import { z } from "zod";
import { defineRoute } from "zodify-express";

const app = express();
app.use(express.json());

app.post(
  "/users",
  defineRoute({
    body: z.object({
      name: z.string().min(1),
      email: z.string().email(),
    }),
    handler(req, res) {
      // ✅ req.body is { name: string; email: string } — fully typed!
      res.json({ message: `Welcome, ${req.body.name}!` });
    },
  })
);

That's it. If the body doesn't match the schema, the client gets a structured 400 response automatically:

{
  "success": false,
  "message": "Validation failed",
  "errors": [
    { "path": "email", "message": "Invalid email" }
  ]
}

👤 Custom Request Properties (req.user, etc.)

Most real apps have auth middleware that attaches a user object to the request. Use createRouteDefiner to tell TypeScript about it — once — and every route handler gets the types for free:

import { createRouteDefiner } from "zodify-express";
import { z } from "zod";

// 1. Describe what your middleware adds to `req`
const defineRoute = createRouteDefiner<{
  user: { id: string; role: "admin" | "member" };
}>();

// 2. Use it everywhere — req.user is auto-inferred
export const getProfile = defineRoute({
  params: z.object({ id: z.string().uuid() }),
  handler(req, res) {
    req.user.id;     // ✅ string
    req.user.role;   // ✅ "admin" | "member"
    req.params.id;   // ✅ string (Zod-validated UUID)

    res.json({ profile: req.user });
  },
});

You can add as many custom properties as you need:

const defineRoute = createRouteDefiner<{
  user: { id: string; role: string };
  session: { token: string; expiresAt: Date };
  requestId: string;
}>();

🎛️ Custom Error Handling

Don't like the default 400 response? Provide your own:

const defineRoute = createRouteDefiner({
  onValidationError(error, req, res, next) {
    // Forward to your global error handler
    next(error);
  },
});

Or return a custom format:

const defineRoute = createRouteDefiner({
  onValidationError(error, req, res) {
    res.status(422).json({
      code: "VALIDATION_ERROR",
      details: error.issues,
    });
  },
});

📖 Full API

defineRoute(config)

A pre-built route definer with no extra request properties. Great for simple apps.

import { defineRoute } from "zodify-express";

| Config Field | Type | Description | |---|---|---| | body | ZodType | Validates req.body | | query | ZodType | Validates req.query | | params | ZodType | Validates req.params | | handler | (req, res, next) => void | Your route logic with fully typed req |

All schema fields are optional — only provided schemas are validated.


createRouteDefiner<TExtra>(options?)

Factory that returns a defineRoute function with custom req properties baked in.

Type Parameter:

| Param | Description | |---|---| | TExtra | An object type describing additional properties on req (e.g. { user: User }) |

Options:

| Option | Type | Default | Description | |---|---|---|---| | onValidationError | (error, req, res, next) => void | Built-in 400 JSON response | Custom validation error handler |


Exported Types

import type {
  ValidatedRequest,   // The augmented Request type
  RouteConfig,        // Config object passed to defineRoute
  ValidationSchemas,  // Just the body/query/params schema fields
  RouteDefinerOptions // Options for createRouteDefiner
} from "zodify-express";

🧩 Patterns & Recipes

Validate query params with coercion

defineRoute({
  query: z.object({
    page: z.coerce.number().min(1).default(1),
    limit: z.coerce.number().min(1).max(100).default(20),
  }),
  handler(req, res) {
    // req.query.page  → number ✅
    // req.query.limit → number ✅
  },
});

Validate route params

defineRoute({
  params: z.object({
    id: z.string().uuid(),
  }),
  handler(req, res) {
    // req.params.id → string (validated UUID) ✅
  },
});

Validate everything at once

defineRoute({
  params: z.object({ id: z.string().uuid() }),
  query: z.object({ include: z.enum(["posts", "comments"]).optional() }),
  body: z.object({ name: z.string(), bio: z.string().max(500) }),
  handler(req, res) {
    // req.params.id        → string
    // req.query.include    → "posts" | "comments" | undefined
    // req.body.name        → string
    // req.body.bio         → string
  },
});

Use with Express Router

import { Router } from "express";
import { z } from "zod";
import { defineRoute } from "zodify-express";

const router = Router();

router.get(
  "/:id",
  defineRoute({
    params: z.object({ id: z.string().uuid() }),
    handler(req, res) {
      res.json({ id: req.params.id });
    },
  })
);

export default router;

🏗️ Real-World Examples

Multiple route definers — public vs. authenticated

The most common pattern: create two route definers, one for public endpoints and one for authenticated endpoints. Each one carries its own type context.

// src/lib/routes.ts
import { createRouteDefiner, defineRoute } from "zodify-express";

// ── Public routes (no auth required) ─────────────────────────
// Use the built-in `defineRoute` — or create one with custom error handling:
export const publicRoute = createRouteDefiner({
  onValidationError(error, _req, res) {
    res.status(422).json({
      code: "VALIDATION_ERROR",
      issues: error.issues,
    });
  },
});

// ── Authenticated routes ─────────────────────────────────────
// Your auth middleware attaches `user` to the request:
export const authRoute = createRouteDefiner<{
  user: { id: string; email: string; role: "admin" | "member" };
}>();

Now use them in your route files:

// src/routes/auth.routes.ts
import { z } from "zod";
import { publicRoute } from "../lib/routes";

export const register = publicRoute({
  body: z.object({
    name: z.string().min(2),
    email: z.string().email(),
    password: z.string().min(8),
  }),
  handler(req, res) {
    // req.body → { name: string; email: string; password: string } ✅
    // req.user → ❌ does NOT exist — this is a public route
    res.status(201).json({ message: `Account created for ${req.body.email}` });
  },
});

export const login = publicRoute({
  body: z.object({
    email: z.string().email(),
    password: z.string(),
  }),
  handler(req, res) {
    // req.body.email → string ✅
    res.json({ token: "jwt-token-here" });
  },
});
// src/routes/user.routes.ts
import { z } from "zod";
import { authRoute } from "../lib/routes";

export const getMe = authRoute({
  handler(req, res) {
    // req.user.id    → string ✅
    // req.user.email → string ✅
    // req.user.role  → "admin" | "member" ✅
    res.json({ user: req.user });
  },
});

export const updateProfile = authRoute({
  body: z.object({
    name: z.string().min(2).optional(),
    bio: z.string().max(500).optional(),
  }),
  handler(req, res) {
    // req.user is fully typed AND req.body is validated
    res.json({ updated: { userId: req.user.id, ...req.body } });
  },
});

Wire it all up:

// src/app.ts
import express from "express";
import { authMiddleware } from "./middleware/auth";
import { register, login } from "./routes/auth.routes";
import { getMe, updateProfile } from "./routes/user.routes";

const app = express();
app.use(express.json());

// Public — no middleware
app.post("/auth/register", register);
app.post("/auth/login", login);

// Authenticated — auth middleware runs first
app.get("/users/me", authMiddleware, getMe);
app.patch("/users/me", authMiddleware, updateProfile);

Role-based route definers

Need different types for different roles? Create a definer per role:

import { createRouteDefiner } from "zodify-express";

type BaseUser = { id: string; email: string };

// Regular users
export const userRoute = createRouteDefiner<{
  user: BaseUser & { role: "member" };
}>();

// Admins get extra fields
export const adminRoute = createRouteDefiner<{
  user: BaseUser & { role: "admin"; permissions: string[] };
}>();
import { z } from "zod";
import { adminRoute } from "../lib/routes";

// Only admins can delete users
export const deleteUser = adminRoute({
  params: z.object({ id: z.string().uuid() }),
  handler(req, res) {
    // req.user.permissions → string[] ✅
    // req.params.id        → string   ✅
    if (!req.user.permissions.includes("users:delete")) {
      return res.status(403).json({ error: "Forbidden" });
    }
    res.json({ deleted: req.params.id });
  },
});

Multi-tenant apps

When your middleware attaches both a user and a tenant:

import { createRouteDefiner } from "zodify-express";

export const tenantRoute = createRouteDefiner<{
  user: { id: string; email: string };
  tenant: { id: string; plan: "free" | "pro" | "enterprise" };
}>();
import { z } from "zod";
import { tenantRoute } from "../lib/routes";

export const createProject = tenantRoute({
  body: z.object({
    name: z.string().min(1).max(100),
    description: z.string().max(1000).optional(),
  }),
  handler(req, res) {
    // req.user.id    → string                          ✅
    // req.tenant.plan → "free" | "pro" | "enterprise"  ✅
    // req.body.name  → string                          ✅

    if (req.tenant.plan === "free") {
      return res.status(403).json({ error: "Upgrade to create projects" });
    }

    res.status(201).json({
      project: {
        name: req.body.name,
        owner: req.user.id,
        tenant: req.tenant.id,
      },
    });
  },
});

Combining with existing Express middleware

defineRoute returns a standard Express handler, so it slots in naturally alongside any middleware:

import express from "express";
import rateLimit from "express-rate-limit";
import { z } from "zod";
import { publicRoute, authRoute } from "./lib/routes";
import { authMiddleware } from "./middleware/auth";

const app = express();
app.use(express.json());

const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10 });

// Stack middleware as usual — defineRoute is just another handler
app.post(
  "/auth/login",
  loginLimiter,
  publicRoute({
    body: z.object({
      email: z.string().email(),
      password: z.string(),
    }),
    handler(req, res) {
      res.json({ token: "..." });
    },
  })
);

app.delete(
  "/posts/:id",
  authMiddleware,
  authRoute({
    params: z.object({ id: z.string().uuid() }),
    handler(req, res) {
      // authMiddleware already verified the user
      // defineRoute validated the UUID param
      res.json({ deleted: req.params.id, by: req.user.id });
    },
  })
);

⚙️ How It Works

   Client Request
        │
        ▼
  ┌─────────────┐
  │  Express    │
  │  Middleware │  ← your auth middleware adds req.user
  └──────┬──────┘
         │
         ▼
  ┌─────────────┐     ┌──────────────┐
  │  defineRoute│────▶│  Zod Parse   │  ← validates body/query/params
  └──────┬──────┘     └──────┬───────┘
         │                   │
         │  validation ok    │  validation fails
         ▼                   ▼
  ┌─────────────┐     ┌──────────────┐
  │  handler()  │     │  400 JSON    │  ← or your custom error handler
  │  fully typed│     │  response    │
  └─────────────┘     └──────────────┘

📄 License

MIT — use it however you like.