Skip to main content

Overview

Express Zod API provides two approaches for integrating native Express middlewares, depending on their purpose and scope. This guide shows you how to use both methods effectively.

Two Integration Methods

1. Global Middlewares with beforeRouting

Use this for middlewares that:
  • Establish their own routes (like Swagger UI)
  • Globally modify behavior
  • Parse additional request formats (like cookies)
  • Need to run before all API routes

2. Endpoint-Specific with addExpressMiddleware()

Use this for middlewares that:
  • Process specific endpoints
  • Need to provide context to handlers
  • Require error transformation
  • Should be tested with your endpoints

Global Middlewares: beforeRouting

The beforeRouting option runs code before your routing is established.

Basic Usage

import { createConfig } from "express-zod-api";
import cookieParser from "cookie-parser";

const config = createConfig({
  beforeRouting: ({ app, getLogger }) => {
    const logger = getLogger();
    
    // Add cookie parser
    app.use(cookieParser());
    
    logger.info("Cookie parser enabled");
  },
});

Serving Documentation

Serve your API documentation using Swagger UI:
import { createConfig } from "express-zod-api";
import ui from "swagger-ui-express";
import { documentation } from "./documentation";

const config = createConfig({
  beforeRouting: ({ app, getLogger }) => {
    const logger = getLogger();
    
    logger.info("Serving API docs at https://example.com/docs");
    app.use("/docs", ui.serve, ui.setup(documentation));
  },
});

Custom Routes

Add custom routes outside your main routing structure:
const config = createConfig({
  beforeRouting: ({ app }) => {
    // Health check endpoint
    app.get("/health", (req, res) => {
      res.json({ status: "ok", timestamp: Date.now() });
    });
    
    // Metrics endpoint
    app.get("/metrics", async (req, res) => {
      const metrics = await collectMetrics();
      res.json(metrics);
    });
  },
});

Using Child Logger

Access request-specific child loggers when configured:
import { createConfig } from "express-zod-api";
import { randomUUID } from "node:crypto";

const config = createConfig({
  childLoggerProvider: ({ parent, request }) =>
    parent.child({ requestId: randomUUID() }),
    
  beforeRouting: ({ app, getLogger }) => {
    app.use("/custom", (req, res, next) => {
      const childLogger = getLogger(req);
      childLogger.info("Custom route accessed");
      res.send("OK");
    });
  },
});

Endpoint-Specific: addExpressMiddleware()

For middlewares that need to interact with specific endpoints, use the addExpressMiddleware() method (alias: use()).

Basic Usage

import { defaultEndpointsFactory } from "express-zod-api";
import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
});

const rateLimitedFactory = defaultEndpointsFactory
  .addExpressMiddleware(limiter);

const endpoint = rateLimitedFactory.build({
  method: "post",
  input: z.object({ data: z.string() }),
  output: z.object({ success: z.boolean() }),
  handler: async () => ({ success: true }),
});

OAuth2 Integration

Integrate OAuth2 JWT bearer authentication:
import { defaultEndpointsFactory } from "express-zod-api";
import createHttpError from "http-errors";
import { auth } from "express-oauth2-jwt-bearer";

const jwtCheck = auth({
  audience: process.env.AUTH0_AUDIENCE,
  issuerBaseURL: process.env.AUTH0_ISSUER,
});

const authenticatedFactory = defaultEndpointsFactory.use(jwtCheck, {
  // Provide context from the middleware
  provider: (req) => ({ 
    auth: req.auth,
    userId: req.auth?.payload.sub,
  }),
  
  // Transform errors to HttpError
  transformer: (err) => createHttpError(401, err.message),
});

const protectedEndpoint = authenticatedFactory.build({
  method: "get",
  input: z.object({}),
  output: z.object({ 
    userId: z.string(),
    data: z.string(),
  }),
  handler: async ({ ctx: { userId } }) => {
    // userId is available from the OAuth middleware
    return {
      userId,
      data: "Protected data",
    };
  },
});

CORS Configuration

While Express Zod API handles CORS natively, you can use the Express middleware for advanced cases:
import cors from "cors";
import { defaultEndpointsFactory } from "express-zod-api";

const corsFactory = defaultEndpointsFactory.addExpressMiddleware(
  cors({
    origin: (origin, callback) => {
      // Custom origin validation logic
      const allowedOrigins = ["https://example.com"];
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    credentials: true,
  }),
  {
    transformer: (err) => createHttpError(403, "CORS error"),
  }
);
Prefer the built-in CORS configuration in most cases—it’s designed to work seamlessly with the framework.

Request Compression

Apply compression to specific endpoints:
import compression from "compression";

const compressedFactory = defaultEndpointsFactory
  .addExpressMiddleware(
    compression({ threshold: "1kb" })
  );

Body Parser Alternatives

Use alternative body parsers for specific content types:
import express from "express";
import { defaultEndpointsFactory } from "express-zod-api";

const textParserFactory = defaultEndpointsFactory
  .addExpressMiddleware(
    express.text({ type: "text/plain" }),
    {
      provider: (req) => ({ rawBody: req.body }),
    }
  );

const textEndpoint = textParserFactory.build({
  method: "post",
  input: z.object({}),
  output: z.object({ length: z.number() }),
  handler: async ({ ctx: { rawBody } }) => ({
    length: rawBody.length,
  }),
});

Helmet Security

Add security headers with Helmet:
import helmet from "helmet";
import { createConfig } from "express-zod-api";

const config = createConfig({
  beforeRouting: ({ app }) => {
    app.use(helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'"],
        },
      },
    }));
  },
});

Request ID Middleware

Add request IDs for tracking:
import { defaultEndpointsFactory } from "express-zod-api";
import { randomUUID } from "node:crypto";

const requestIdMiddleware = (req, res, next) => {
  req.id = randomUUID();
  res.setHeader("X-Request-ID", req.id);
  next();
};

const trackedFactory = defaultEndpointsFactory
  .addExpressMiddleware(requestIdMiddleware, {
    provider: (req) => ({ requestId: req.id }),
  });

Context Providers

The provider option extracts data from the Express request:
import { defaultEndpointsFactory } from "express-zod-api";

const factory = defaultEndpointsFactory.use(someMiddleware, {
  // Synchronous provider
  provider: (req) => ({
    userAgent: req.headers["user-agent"],
    ip: req.ip,
  }),
});

// Or asynchronous
const asyncFactory = defaultEndpointsFactory.use(authMiddleware, {
  provider: async (req) => {
    const user = await db.findUser(req.auth.userId);
    return { user };
  },
});

Error Transformers

Transform Express middleware errors to HTTP errors:
import createHttpError from "http-errors";
import { defaultEndpointsFactory } from "express-zod-api";

const factory = defaultEndpointsFactory.use(someMiddleware, {
  transformer: (err) => {
    // Map specific error types
    if (err.name === "UnauthorizedError") {
      return createHttpError(401, "Authentication failed");
    }
    if (err.name === "ForbiddenError") {
      return createHttpError(403, "Access denied");
    }
    // Default to 500
    return createHttpError(500, err.message);
  },
});

Multiple Middlewares

Chain multiple Express middlewares:
import { defaultEndpointsFactory } from "express-zod-api";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import { auth } from "express-oauth2-jwt-bearer";

const secureFactory = defaultEndpointsFactory
  .use(helmet())
  .use(rateLimit({ windowMs: 900000, max: 100 }))
  .use(auth({ /* config */ }), {
    provider: (req) => ({ auth: req.auth }),
    transformer: (err) => createHttpError(401, err.message),
  });

Avoid: CORS Middleware

Don’t use Express CORS middleware in beforeRouting—use the framework’s built-in option instead:
// ❌ Don't do this
import cors from "cors";

const config = createConfig({
  beforeRouting: ({ app }) => {
    app.use(cors()); // Not recommended
  },
});

// ✅ Do this instead
const config = createConfig({
  cors: true, // or function for custom headers
});

Static File Serving

Serve static files without Express middleware:
import { Routing, ServeStatic } from "express-zod-api";

const routing: Routing = {
  public: new ServeStatic("assets", {
    dotfiles: "deny",
    index: false,
    redirect: false,
  }),
};

Passport.js Integration

Integrate Passport authentication strategies:
import passport from "passport";
import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt";
import { createConfig, defaultEndpointsFactory } from "express-zod-api";

// Configure Passport
passport.use(
  new JwtStrategy(
    {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    },
    async (payload, done) => {
      const user = await db.Users.findById(payload.sub);
      return done(null, user || false);
    }
  )
);

const config = createConfig({
  beforeRouting: ({ app }) => {
    app.use(passport.initialize());
  },
});

const authenticatedFactory = defaultEndpointsFactory.use(
  passport.authenticate("jwt", { session: false }),
  {
    provider: (req) => ({ user: req.user }),
    transformer: (err) => createHttpError(401, "Unauthorized"),
  }
);

Best Practices

Choose the Right Method

Use beforeRouting for global setup and addExpressMiddleware() for endpoint-specific logic.

Provide Context

Always use the provider option to make middleware data available to handlers.

Transform Errors

Use the transformer option to convert Express errors to HttpError instances.

Prefer Native Features

Use the framework’s built-in features for CORS, compression, and file uploads when possible.

Testing

Test endpoints with Express middlewares:
import { testEndpoint } from "express-zod-api";

describe("rateLimitedEndpoint", () => {
  test("should work with rate limiting", async () => {
    const { responseMock, loggerMock } = await testEndpoint({
      endpoint: rateLimitedEndpoint,
      requestProps: {
        method: "POST",
        body: { data: "test" },
      },
    });
    
    expect(responseMock._getStatusCode()).toBe(200);
    expect(loggerMock._getLogs().error).toHaveLength(0);
  });
});

Next Steps