If you already have an Express application or need more control over your server configuration, you can integrate Express Zod API into your existing setup.
Why Integrate?
You might want to integrate Express Zod API with your own Express app when:
- You have an existing Express application
- You need custom middleware not provided by the framework
- You want to mix Express Zod API endpoints with traditional Express routes
- You need fine-grained control over server configuration
- Youβre migrating an existing API gradually
Basic Integration
Use attachRouting() to connect your endpoints to an existing Express app:
import express from "express";
import { createConfig, attachRouting, Routing } from "express-zod-api";
import { routing } from "./routing";
const app = express();
const config = createConfig({
app, // Provide your Express app
cors: true,
logger: { level: "debug" },
});
const { notFoundHandler, logger } = attachRouting(config, routing);
// Optional: Handle 404 errors
app.use(notFoundHandler);
// Start your server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
logger.info(`Server listening on port ${PORT}`);
});
When using your own Express app, you need to:
- Parse
request.body yourself (e.g., with express.json())
- Call
app.listen() yourself
- Handle 404 errors yourself (using the provided
notFoundHandler)
With Express Router
You can also attach routing to an Express Router:
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
import { apiRouting } from "./api-routing";
const app = express();
const apiRouter = express.Router();
const config = createConfig({
app: apiRouter, // Attach to router instead
});
attachRouting(config, apiRouting);
// Mount the router on a prefix
app.use("/api", apiRouter);
app.listen(8080);
Request Body Parsing
Express Zod API needs parsed request bodies. Set up parsers before attaching routing:
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
const app = express();
// Parse JSON bodies
app.use(express.json());
// Parse URL-encoded bodies (for forms)
app.use(express.urlencoded({ extended: true }));
// Parse raw bodies (for binary data)
app.use(express.raw({ type: "application/octet-stream" }));
const config = createConfig({ app });
const { notFoundHandler } = attachRouting(config, routing);
app.use(notFoundHandler);
app.listen(8080);
If you donβt provide an app in config, Express Zod API automatically sets up these parsers for you.
Mixing with Traditional Routes
Combine Express Zod API endpoints with traditional Express routes:
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
import { apiRouting } from "./api-routing";
const app = express();
app.use(express.json());
// Traditional Express route
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: Date.now() });
});
// Express Zod API routes
const config = createConfig({ app });
const { notFoundHandler } = attachRouting(config, apiRouting);
// More traditional routes
app.get("/version", (req, res) => {
res.json({ version: "1.0.0" });
});
// 404 handler should be last
app.use(notFoundHandler);
app.listen(8080);
Static File Serving
Serve static files alongside your API:
import express from "express";
import { createConfig, attachRouting, Routing, ServeStatic } from "express-zod-api";
import path from "path";
const app = express();
// Serve static files from public directory
app.use("/static", express.static(path.join(__dirname, "public")));
// You can also use ServeStatic in routing
const routing: Routing = {
api: {
// Your API endpoints
},
// Serve files from ./assets at /public
public: new ServeStatic("assets", {
dotfiles: "deny",
index: false,
}),
};
const config = createConfig({ app });
attachRouting(config, routing);
app.listen(8080);
Custom Middleware
Add middleware that runs before Express Zod API routes:
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
const app = express();
// Security middleware
app.use(helmet());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
});
app.use(limiter);
// Request logging
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
const config = createConfig({ app });
attachRouting(config, routing);
app.listen(8080);
Using beforeRouting Option
For middleware that needs access to the logger or should run right before routing:
import { createConfig, attachRouting } from "express-zod-api";
import swaggerUi from "swagger-ui-express";
import { readFileSync } from "fs";
import YAML from "yaml";
const spec = YAML.parse(readFileSync("openapi.yaml", "utf-8"));
const config = createConfig({
beforeRouting: ({ app, getLogger }) => {
const logger = getLogger();
// Serve API documentation
logger.info("Serving API docs at /docs");
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
// Custom middleware with logger access
app.use("/api", (req, res, next) => {
logger.debug(`API request: ${req.method} ${req.path}`);
next();
});
},
});
attachRouting(config, routing);
Error Handling
Implement custom error handling:
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
const app = express();
app.use(express.json());
const config = createConfig({ app });
const { notFoundHandler, logger } = attachRouting(config, routing);
// 404 handler
app.use(notFoundHandler);
// Global error handler (must be last)
app.use((err, req, res, next) => {
logger.error("Unhandled error:", err);
res.status(err.statusCode || 500).json({
status: "error",
message: process.env.NODE_ENV === "production"
? "Internal server error"
: err.message,
});
});
app.listen(8080);
HTTPS with Custom Server
Create an HTTPS server with your own configuration:
import express from "express";
import https from "https";
import { readFileSync } from "fs";
import { createConfig, attachRouting } from "express-zod-api";
const app = express();
app.use(express.json());
const config = createConfig({ app });
const { logger } = attachRouting(config, routing);
const httpsOptions = {
key: readFileSync("privkey.pem"),
cert: readFileSync("fullchain.pem"),
};
const server = https.createServer(httpsOptions, app);
server.listen(443, () => {
logger.info("HTTPS server running on port 443");
});
WebSocket Support
Combine with WebSocket libraries:
import express from "express";
import { createServer } from "http";
import { WebSocketServer } from "ws";
import { createConfig, attachRouting } from "express-zod-api";
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
app.use(express.json());
const config = createConfig({ app });
attachRouting(config, routing);
// WebSocket handling
wss.on("connection", (ws) => {
console.log("WebSocket client connected");
ws.on("message", (data) => {
console.log("Received:", data.toString());
});
});
server.listen(8080);
For a fully type-safe WebSocket solution, consider Zod Sockets, a companion framework by the same author.
Database Connections
Initialize database connections before starting the server:
import express from "express";
import mongoose from "mongoose";
import { createConfig, attachRouting } from "express-zod-api";
const app = express();
app.use(express.json());
async function start() {
// Connect to database
await mongoose.connect(process.env.MONGODB_URI!);
console.log("Connected to MongoDB");
const config = createConfig({ app });
const { logger } = attachRouting(config, routing);
app.listen(8080, () => {
logger.info("Server started on port 8080");
});
}
start().catch(console.error);
Gradual Migration
Migrate an existing API gradually:
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
import { legacyRoutes } from "./legacy-routes";
import { newApiRouting } from "./new-api-routing";
const app = express();
app.use(express.json());
// Mount legacy routes
app.use("/api/v1", legacyRoutes);
// Mount new Express Zod API routes
const config = createConfig({ app });
const { notFoundHandler } = attachRouting(config, newApiRouting);
app.use(notFoundHandler);
app.listen(8080);
Your routing file:
import { Routing } from "express-zod-api";
import { getUserEndpoint, updateUserEndpoint } from "./endpoints";
export const newApiRouting: Routing = {
api: {
v2: { // New version
user: {
":id": {
get: getUserEndpoint,
patch: updateUserEndpoint,
},
},
},
},
};
Testing
Test your integrated application:
import request from "supertest";
import express from "express";
import { createConfig, attachRouting } from "express-zod-api";
import { routing } from "./routing";
describe("API Integration", () => {
let app: express.Application;
beforeEach(() => {
app = express();
app.use(express.json());
const config = createConfig({
app,
logger: { level: "silent" }, // Silence logs during tests
});
attachRouting(config, routing);
});
it("should respond to health check", async () => {
const response = await request(app)
.get("/health")
.expect(200);
expect(response.body).toEqual({ status: "ok" });
});
it("should handle API endpoint", async () => {
const response = await request(app)
.get("/api/user/123")
.expect(200);
expect(response.body.data).toHaveProperty("id", "123");
});
});
Complete Example
Hereβs a complete integration example with many features:
import express from "express";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import swaggerUi from "swagger-ui-express";
import YAML from "yaml";
import { readFileSync } from "fs";
import { createConfig, attachRouting } from "express-zod-api";
import { apiRouting } from "./routing";
const app = express();
// Security
app.use(helmet());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
});
app.use("/api", limiter);
// Body parsing
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true }));
// Serve documentation
const spec = YAML.parse(readFileSync("openapi.yaml", "utf-8"));
app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec));
// Health check
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: Date.now() });
});
// Attach Express Zod API routing
const config = createConfig({
app,
cors: true,
logger: { level: "info" },
});
const { notFoundHandler, logger } = attachRouting(config, apiRouting);
// 404 handler
app.use(notFoundHandler);
// Error handler
app.use((err, req, res, next) => {
logger.error("Error:", err);
res.status(500).json({
status: "error",
message: "Internal server error",
});
});
// Start server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
logger.info(`π Server running on http://localhost:${PORT}`);
logger.info(`π Documentation at http://localhost:${PORT}/docs`);
});
Next Steps