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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@minisylar/express-typed-router

v1.6.2

Published

A strongly-typed Express router with Zod validation and automatic type inference for params, body, query, and middleware

Downloads

97

Readme

@minisylar/express-typed-router

A strongly-typed Express router with schema validation and automatic type inference for params, body, query, and middleware.

Features

  • 🚀 Full TypeScript support with automatic type inference for route parameters
  • 🛡️ Schema validation for request body, query parameters, and route params (Zod, Yup, Valibot, Arktype,Joi,Effect,decoders, ts.data.json, unhoax, etc.)
  • 🔗 Express.js compatibility - works with Express 4 and Express 5
  • 🤝 Mix with existing Express routes - seamlessly integrates with your current codebase
  • 📝 JSDoc documentation with comprehensive examples
  • 📦 ES Modules and CommonJS support
  • 🎯 Zero runtime overhead for type checking

Installation

npm install @minisylar/express-typed-router
# or
pnpm add @minisylar/express-typed-router
# or
yarn add @minisylar/express-typed-router

Note: This package requires Express 4.18.0+ or Express 5.0.0+. For schema validation the library works with multiple popular schema libraries (examples below).

Schema Compatibility

This library is schema-agnostic: it provides a validation plumbing that works with multiple popular schema libraries. Below are short examples showing how you can use different schema libraries with the router. The router expects a schema-like object that can validate input; most adapters are straightforward.

Example with Zod (v3 or v4):

import { z } from "zod"; // or "zod/v4" or "zod/v3" as needed
const userSchema = z.object({ name: z.string() });
router.post("/users", { bodySchema: userSchema }, handler);

Example with Yup:

import * as yup from "yup";
const userSchema = yup.object({ name: yup.string().required() });
// pass the yup schema directly as bodySchema; the router will run validation
router.post("/users", { bodySchema: userSchema }, handler);

Example with Valibot (valibot):

import { object, string } from "valibot";
const userSchema = object({ name: string() });
router.post("/users", { bodySchema: userSchema }, handler);

Example with Arktype:

import { object, string } from "arktype";
const userSchema = object({ name: string() });
router.post("/users", { bodySchema: userSchema }, handler);

If a schema library needs an adapter (for example to map its errors to the router's error format), add a small wrapper that runs validation and throws the expected error shape. See the project's examples for concrete adapter patterns.

Note about Joi: Joi's TypeScript typings do not reliably infer the output type from the runtime schema shape. When using Joi you should either:

  • provide an explicit generic type for the schema (e.g. Joi.object<MyType>(...)),
  • add a variable type annotation (e.g. const s: Joi.ObjectSchema<MyType> = Joi.object(...)), or
  • write a small adapter that validates at runtime and exposes a typed result to TypeScript.

Quick Start

import express from "express";
import { z } from "zod";
import { createTypedRouter } from "@minisylar/express-typed-router";

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

// Create a typed router
const router = createTypedRouter();

// Define routes - parameters are automatically typed!
router.get("/users/:userId", (req, res) => {
  // req.params.userId is automatically inferred as string
  res.json({ userId: req.params.userId });
});

// Add validation with Zod schemas
router.post(
  "/users",
  {
    bodySchema: z.object({
      name: z.string(),
      email: z.string().email(),
    }),
  },
  (req, res) => {
    // req.body is validated and typed automatically
    const { name, email } = req.body;
    res.json({ id: "123", name, email });
  }
);

const expressRouter = router.getRouter();
app.use("/api", expressRouter);
app.listen(3000);

That's it! Your routes now have full type safety and validation.

Works with Existing Express Routes

The typed router seamlessly integrates with your existing Express application - no need to rewrite everything!

import express from "express";
import { createTypedRouter } from "@minisylar/express-typed-router";

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

// Your existing Express routes continue to work
app.get("/health", (req, res) => {
  res.json({ status: "ok" });
});

// Existing Express router
const legacyRouter = express.Router();
legacyRouter.get("/legacy/:id", (req, res) => {
  res.json({ id: req.params.id });
});

// New typed router with full type safety
const typedRouter = createTypedRouter();
typedRouter.get("/users/:userId", (req, res) => {
  // req.params.userId is automatically typed as string
  res.json({ userId: req.params.userId });
});

// Extract the Express router before using it
const typedExpressRouter = typedRouter.getRouter();

// Mix them all together
app.use("/api/legacy", legacyRouter);
app.use("/api/v2", typedExpressRouter);

// Gradually migrate your routes to get type safety where you need it!
app.listen(3000);

The Main API: createTypedRouter()

createTypedRouter() is the primary and most flexible way to create typed routers. It supports:

  • Global middleware with automatic type merging
  • Per-route middleware with type inference
  • Zod validation for params, body, and query
  • Express 4 & 5 compatibility with full route pattern support
  • Chainable API for easy configuration

Global Middleware

Important: Unlike Express, middleware must be applied using method chaining or capturing returned routers. See the FAQ section for details.

// Method chaining pattern (recommended)
const router = createTypedRouter()
  .useMiddleware(authMiddleware)
  .useMiddleware(loggingMiddleware)
  .useMiddleware(timestampMiddleware);

// All routes automatically get types from all middleware
router.get("/protected", (req, res) => {
  // req.userId, req.requestId, req.timestamp all available and typed
});

// Alternative: capturing returned router
const baseRouter = createTypedRouter();
const routerWithMiddleware = baseRouter
  .useMiddleware(authMiddleware)
  .useMiddleware(loggingMiddleware);

// Use the router with middleware applied
routerWithMiddleware.get("/users", handler);

Per-Route Middleware

router.get(
  "/admin/:userId",
  {
    middleware: [adminMiddleware, auditMiddleware],
  },
  (req, res) => {
    // Types from both global AND per-route middleware are merged
    // req.userId (global), req.isAdmin (adminMiddleware), req.auditId (auditMiddleware)
  }
);

Express 4 & 5 Route Pattern Support

Works with all Express routing patterns:

// Named parameters
router.get("/users/:userId", handler); // { userId: string }

// Multiple parameters
router.get("/users/:userId/posts/:postId", handler); // { userId: string; postId: string }

// Consecutive parameters with separators
router.get("/flights/:from-:to", handler); // { from: string; to: string }
router.get("/files/:name.:ext", handler); // { name: string; ext: string }

// Optional parameters (Express 4)
router.get("/posts/:year/:month?", handler); // { year: string; month?: string }

// Repeating parameters (Express 5)
router.get("/files/:path+", handler); // { path: string[] }

// Optional repeating (Express 5)
router.get("/search/:terms*", handler); // { terms?: string[] }

// Optional segments (Express 5)
router.get("/api{/:version}/users", handler); // { version?: string }

// Regex constraints
router.get("/users/:id(\\d+)", handler); // { id: string }

// Wildcards
router.get("/static/*", handler); // { "0": string }

Comprehensive Example

import express from "express";
import { z } from "zod";
import { createTypedRouter } from "@minisylar/express-typed-router";

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

// Create router and define middleware inline (automatically typed!)
const router = createTypedRouter()
  .useMiddleware((req, res, next) => {
    const token = req.headers.authorization;
    req.userId = "user123";
    req.isAdmin = token?.includes("admin") || false;
    next();
  })
  .useMiddleware((req, res, next) => {
    req.requestId = Math.random().toString(36);
    console.log(`[${req.requestId}] ${req.method} ${req.path}`);
    next();
  });

// Define schemas
const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(["user", "admin"]).optional(),
});

const UserQuerySchema = z.object({
  include: z.array(z.string()).optional(),
  limit: z.coerce.number().int().positive().max(100).default(10),
});

// Routes with full type safety
router.get(
  "/users/:userId",
  {
    querySchema: UserQuerySchema,
  },
  (req, res) => {
    // All properties are automatically typed:
    const { userId } = req.params; // string (auto-inferred from route)
    const { include, limit } = req.query; // from schema validation
    const { userId: authUserId, isAdmin, requestId } = req; // from middleware

    res.json({
      id: userId,
      authUserId,
      isAdmin,
      requestId,
      include,
      limit,
    });
  }
);

router.post(
  "/users",
  {
    bodySchema: CreateUserSchema,
  },
  (req, res) => {
    const { name, email, role } = req.body; // Fully typed from schema
    const { userId, isAdmin, requestId } = req; // From middleware

    if (role === "admin" && !isAdmin) {
      return res.status(403).json({ error: "Insufficient permissions" });
    }

    res.status(201).json({
      id: "new-user-id",
      name,
      email,
      role: role || "user",
      createdBy: userId,
      requestId,
    });
  }
);

// Per-route middleware can also be inline
router.delete(
  "/users/:userId",
  {
    middleware: [
      (req, res, next) => {
        if (!req.isAdmin) {
          return res.status(403).json({ error: "Admin required" });
        }
        req.hasAdminAccess = true;
        next();
      },
    ], // No need for 'as const' - middleware arrays are automatically typed
  },
  (req, res) => {
    // Types from BOTH global middleware AND per-route middleware
    const { userId } = req.params; // From route
    const { userId: authUserId, requestId } = req; // From global middleware
    const { hasAdminAccess } = req; // From per-route middleware

    res.json({
      deleted: userId,
      deletedBy: authUserId,
      requestId,
      hasAdminAccess,
    });
  }
);

app.use("/api", router.getRouter());
app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

With Explicit TypeScript Types

For TypeScript users who prefer explicit typing, you can define middleware with generics:

import { TypedMiddleware } from "@minisylar/express-typed-router";

// Define typed middleware explicitly
const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
  req,
  res,
  next
) => {
  const token = req.headers.authorization;
  req.userId = "user123";
  req.isAdmin = token?.includes("admin") || false;
  next();
};

const loggingMiddleware: TypedMiddleware<{ requestId: string }> = (
  req,
  res,
  next
) => {
  req.requestId = Math.random().toString(36);
  console.log(`[${req.requestId}] ${req.method} ${req.path}`);
  next();
};

// Use the explicitly typed middleware
const router = createTypedRouter()
  .useMiddleware(authMiddleware)
  .useMiddleware(loggingMiddleware);

// Rest of the routes work the same way...

TypeScript Features

For TypeScript users, the library provides advanced type safety features:

Typed Middleware

Define middleware that extends the request object with typed properties:

import { TypedMiddleware } from "@minisylar/express-typed-router";

const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
  req,
  res,
  next
) => {
  req.userId = "user123";
  req.isAdmin = true;
  next();
};

// Add to router - types are automatically merged
router.useMiddleware(authMiddleware);

router.get("/protected", (req, res) => {
  // TypeScript knows about req.userId and req.isAdmin
  const { userId, isAdmin } = req;
  res.json({ userId, isAdmin });
});

Per-Route Middleware with Type Merging

const adminMiddleware: TypedMiddleware<{ hasAdminAccess: true }> = (
  req,
  res,
  next
) => {
  if (!req.isAdmin) {
    return res.status(403).json({ error: "Admin required" });
  }
  req.hasAdminAccess = true;
  next();
};

router.get(
  "/admin/:userId",
  {
    middleware: [adminMiddleware], // No need for 'as const' - arrays are automatically typed
  },
  (req, res) => {
    // Types from BOTH global and per-route middleware are available
    const { userId } = req.params; // From route params
    const { userId: authUserId } = req; // From global middleware
    const { hasAdminAccess } = req; // From per-route middleware

    res.json({ userId, authUserId, hasAdminAccess });
  }
);

Advanced Route Parameter Types

The library automatically infers complex Express route patterns:

// Express 5 repeating parameters
router.get("/files/:path+", (req, res) => {
  const { path } = req.params; // string[] - automatically inferred!
});

// Optional parameters
router.get("/posts/:year/:month?", (req, res) => {
  const { year, month } = req.params; // { year: string; month?: string }
});

// Complex patterns with separators
router.get("/flights/:from-:to", (req, res) => {
  const { from, to } = req.params; // { from: string; to: string }
});

Alternative API Styles

For developers who prefer setting up all middleware upfront:

import {
  createTypedRouterWithMiddleware,
  TypedMiddleware,
} from "@minisylar/express-typed-router";

const authMiddleware: TypedMiddleware<{ userId: string; isAdmin: boolean }> = (
  req,
  res,
  next
) => {
  req.userId = "user123";
  req.isAdmin = true;
  next();
};

const timestampMiddleware: TypedMiddleware<{ timestamp: Date }> = (
  req,
  res,
  next
) => {
  req.timestamp = new Date();
  next();
};

// Create router with middleware - types are automatically merged
const router = createTypedRouterWithMiddleware(
  authMiddleware,
  timestampMiddleware
);

router.get("/protected", (req, res) => {
  // req.userId, req.isAdmin, and req.timestamp are all typed correctly!
  res.json({
    userId: req.userId, // string
    isAdmin: req.isAdmin, // boolean
    timestamp: req.timestamp, // Date
  });
});

For applications that need custom error handling or configuration:

import { createTypedRouterWithConfig } from "@minisylar/express-typed-router";

const router = createTypedRouterWithConfig({
  errorHandler: (error, req, res, next) => {
    if (error.name === "ZodError") {
      res.status(400).json({
        error: "Validation failed",
        details: error.errors,
      });
    } else {
      next(error);
    }
  },
});

// Use normally
router.get("/users/:id", (req, res) => {
  // Custom error handling is automatically applied
  const { id } = req.params;
  res.json({ id });
});

Express 4 & 5 Route Pattern Support

This library provides complete TypeScript inference for all Express.js routing patterns across both Express 4 and 5:

Basic Patterns (Express 4 & 5)

// Named parameters
router.get("/users/:userId", handler);
// → { userId: string }

// Multiple parameters
router.get("/users/:userId/posts/:postId", handler);
// → { userId: string; postId: string }

// Parameters with separators
router.get("/flights/:from-:to", handler);
// → { from: string; to: string }

router.get("/files/:name.:ext", handler);
// → { name: string; ext: string }

Advanced Patterns (Express 4)

// Optional parameters
router.get("/posts/:year/:month?", handler);
// → { year: string; month?: string }

// Regex constraints
router.get("/users/:id(\\d+)", handler);
// → { id: string }

// Wildcards
router.get("/files/*", handler);
// → { "0": string }

router.get("/api/*/files/*", handler);
// → { "0": string; "1": string }

Express 5 Enhanced Patterns

// Repeating parameters (one or more)
router.get("/files/:path+", handler);
// → { path: string[] }

// Optional repeating (zero or more)
router.get("/search/:terms*", handler);
// → { terms?: string[] }

// Optional segments with braces
router.get("/api{/:version}/users", handler);
// → { version?: string }

router.get("/files{/:category}/:filename", handler);
// → { category?: string; filename: string }

Real-World Examples

// E-commerce routes
router.get("/products/:category/:subcategory?", handler);
// → { category: string; subcategory?: string }

// File serving with optional versioning
router.get("/assets{/:version}/:filename.:ext", handler);
// → { version?: string; filename: string; ext: string }

// API versioning with wildcards
router.get("/api/v:version/*", handler);
// → { version: string; "0": string }

// Multi-segment paths (Express 5)
router.get("/docs/:sections+", handler);
// → { sections: string[] }

All patterns work seamlessly with Zod validation and middleware type inference!

API Reference

createTypedRouter() - Main API

Creates a typed router instance with full flexibility for middleware and validation.

const router = createTypedRouter();

// Add global middleware (chainable)
router.useMiddleware(middleware1)
      .useMiddleware(middleware2);

// All HTTP methods supported
router.get(path, options?, handler)
router.post(path, options?, handler)
router.put(path, options?, handler)
router.patch(path, options?, handler)
router.delete(path, options?, handler)
router.options(path, options?, handler)
router.head(path, options?, handler)
router.all(path, options?, handler)

Route Options:

  • bodySchema: Zod schema for request body validation
  • querySchema: Zod schema for query parameter validation
  • paramsSchema: Zod schema for route parameter validation (optional - auto-inferred from path)
  • middleware: Array of typed middleware functions for this specific route

Examples:

// Simple route with auto-inferred params
router.get("/users/:id", (req, res) => {
  const { id } = req.params; // string
});

// With body validation
router.post(
  "/users",
  {
    bodySchema: z.object({ name: z.string() }),
  },
  (req, res) => {
    const { name } = req.body; // string
  }
);

// With per-route middleware
router.get(
  "/admin",
  {
    middleware: [authMiddleware, adminMiddleware] as const,
  },
  (req, res) => {
    // Types from both middleware are available
  }
);

TypedMiddleware<T>

Type for middleware functions that extend the request object.

const authMiddleware: TypedMiddleware<{ userId: string }> = (
  req,
  res,
  next
) => {
  req.userId = "123";
  next();
};

createTypedRouterWithConfig(config)

const router = createTypedRouterWithConfig({
  errorHandler: (error, req, res, next) => {
    // Custom error handling
  },
});

createTypedRouterWithMiddleware(...middleware)

const router = createTypedRouterWithMiddleware(middleware1, middleware2);

Development

# Install dependencies
pnpm install

# Build the library
pnpm build

# Run type checking
pnpm type-check

# Build in watch mode
pnpm build:watch

License

ISC

FAQ and Common Patterns

Middleware Behavior Differences from Express

IMPORTANT: Router Middleware and Route Registration

When using middleware with express-typed-router, there's an important difference from standard Express behavior:

In Express, middleware added with router.use() applies to all routes registered after it:

// Express middleware behavior
const router = express.Router();
router.use(authMiddleware); // Apply middleware
router.get("/route1", handler1); // Has authMiddleware
router.use(logMiddleware); // Apply another middleware
router.get("/route2", handler2); // Has BOTH auth and log middleware

In express-typed-router, useMiddleware() returns a new router instance for type safety:

// ❌ WON'T WORK - middleware not applied to route
const router = createTypedRouter();
router.useMiddleware(authMiddleware); // Returns new router that isn't captured
router.get("/route", handler); // Original router without middleware!

// ✅ CORRECT - chain methods (recommended)
const router = createTypedRouter()
  .useMiddleware(authMiddleware)
  .get("/route", handler);

// ✅ CORRECT - chain directly from middleware call
const router = createTypedRouter();
router.useMiddleware(authMiddleware).get("/route", handler);

// ✅ ALSO CORRECT - use per-route middleware
const router = createTypedRouter();
router.get("/route", { middleware: [authMiddleware] }, handler);

This design is necessary for full type safety but requires a different pattern than standard Express.

Common Express Patterns vs express-typed-router

Here are common Express patterns and how to achieve them with express-typed-router:

Pattern 1: Adding middleware to specific routes

Express:

const router = express.Router();
router.get("/public", publicHandler);
router.use(authMiddleware); // Only affects routes below
router.get("/private", privateHandler); // Has authMiddleware

express-typed-router:

// Option 1: Separate routers
const publicRouter = createTypedRouter();
publicRouter.get("/public", publicHandler);

const privateRouter = createTypedRouter().useMiddleware(authMiddleware);
privateRouter.get("/private", privateHandler);

// Combine in Express
app.use(publicRouter.getRouter());
app.use(privateRouter.getRouter());

// Option 2: Per-route middleware
const router = createTypedRouter();
router.get("/public", publicHandler);
router.get("/private", { middleware: [authMiddleware] }, privateHandler);

Pattern 2: Adding middleware for a group of routes

Express:

const router = express.Router();
router.get("/public", handler);

// Only admin routes have auth middleware
const adminRouter = express.Router();
adminRouter.use(authMiddleware);
adminRouter.get("/users", adminHandler1);
adminRouter.get("/settings", adminHandler2);

router.use("/admin", adminRouter);

express-typed-router:

const publicRouter = createTypedRouter();
publicRouter.get("/public", handler);

// Admin router with middleware
const adminRouter = createTypedRouter().useMiddleware(authMiddleware);
adminRouter.get("/users", adminHandler1);
adminRouter.get("/settings", adminHandler2);

// Combine with Express
app.use(publicRouter.getRouter());
app.use("/admin", adminRouter.getRouter());

Pattern 3: Middleware with dynamically added routes

Express:

const router = express.Router();
router.use(middleware);

// Later, routes are added dynamically
function addRoute(path, handler) {
  router.get(path, handler); // Has middleware
}

express-typed-router:

// Option 1: Pass the router to the function
const router = createTypedRouter().useMiddleware(middleware);

function addRoute(router, path, handler) {
  router.get(path, handler);
}

// Option 2: Factory function
function createRouteAdder(middleware) {
  const router = createTypedRouter().useMiddleware(middleware);

  return {
    addRoute: (path, handler) => router.get(path, handler),
    getRouter: () => router.getRouter(),
  };
}

const routeAdder = createRouteAdder(middleware);
routeAdder.addRoute("/path", handler);
app.use(routeAdder.getRouter());

Using express-typed-router in JavaScript

JavaScript users don't need to worry about TypeScript types but should still follow the middleware chaining pattern:

// JavaScript usage
const { createTypedRouter } = require("@minisylar/express-typed-router");

const router = createTypedRouter().useMiddleware((req, res, next) => {
  req.user = { id: "user123" };
  next();
});

router.get("/users", (req, res) => {
  // req.user is available but not typed (JavaScript doesn't have types)
  res.json({ userId: req.user.id });
});

module.exports = router.getRouter();

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.