Server Setup#
Learn how to structure a production-ready Express server with TypeScript, CORS, middleware, and routers.
Server Class Architecture#
The Server class pattern provides a clean, organized way to structure your Express application.
import express, { Router } from "express";
import cors from "cors";
import morgan from "morgan";
import path from "path";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth/auth.js";
import logger from "./utils/logger.js";
import type { Config } from "./config.js";
export class Server {
app: express.Express;
subrouters: { [key: string]: Router } = {};
config: Config;
constructor(config: Config) {
this.app = express();
this.config = config;
// Setup middleware and routes
this.setupCORS();
this.setupLogging();
this.setupAuth();
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandlers();
}
private setupCORS() {
const whitelist = [this.config.frontendUrl, ...this.config.otherAllowedOrigins];
this.app.use(
cors({
origin: function (origin, callback) {
console.log("Request coming from", origin);
if (whitelist.indexOf(origin!) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true, // Allow cookies, authorization headers
})
);
}
private setupLogging() {
this.app.use(
morgan(this.config.devMode ? "dev" : "combined", {
stream: {
write: (msg: string) => {
logger.http(msg.trim());
},
},
})
);
}
private setupAuth() {
// Better Auth handler - MUST come before express.json()
this.app.all("/api/auth/*", toNodeHandler(auth));
}
private setupMiddleware() {
// Mount express.json() AFTER Better Auth handler
this.app.use(express.json());
// Health check endpoint
this.app.get("/api/healthcheck", (req, res) => {
res.json({ status: "available", apiVersion: "v1" });
});
// Static file serving
this.app.use(
"/uploads",
express.static(path.join(process.cwd(), "uploads"))
);
}
private setupRoutes() {
// Define your routers
this.subrouters = {
"/api/users": usersRouter,
"/api/treks": treksRouter,
"/api/posts": postsRouter,
};
// Dev-only routes
if (this.config.devMode) {
this.app.use("/api/tmp", tmpRouter);
}
// Auto-register all subrouters
Object.entries(this.subrouters).forEach(([prefix, router]) => {
this.app.use(prefix, router);
});
}
private setupErrorHandlers() {
// 404 handler (must come before error handler)
this.app.use(handler404);
// Global error handler (must be last)
this.app.use(errorHandler);
}
start() {
this.app.listen(this.config.port, "0.0.0.0", () => {
logger.info(`Server running on 0.0.0.0:${this.config.port}...`);
});
}
}
Usage#
// config.ts
export interface Config {
port: number;
devMode: boolean;
frontendUrl: string;
otherAllowedOrigins: string[];
}
export const config: Config = {
port: Number(process.env.PORT) || 3000,
devMode: process.env.NODE_ENV !== "production",
frontendUrl: process.env.FRONTEND_URL || "http://localhost:5173",
otherAllowedOrigins: process.env.OTHER_ALLOWED_ORIGINS?.split(",").filter(
(u) => u.trim() !== ""
) || [],
};
// index.ts
import { Server } from "./server.js";
import { config } from "./config.js";
const server = new Server(config);
server.start();
CORS Configuration#
Allow Multiple Origins#
// Parse comma-separated origins from environment
const otherAllowedOrigins =
process.env.OTHER_ALLOWED_ORIGINS?.split(",").filter(
(u) => u.trim() !== ""
) || [];
const whitelist = [config.frontendUrl, ...otherAllowedOrigins];
app.use(
cors({
origin: function (origin, callback) {
// Allow requests with no origin (like mobile apps or curl)
if (!origin) return callback(null, true);
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
})
);
Environment Variables#
# .env
FRONTEND_URL=http://localhost:5173
OTHER_ALLOWED_ORIGINS=http://localhost:3001,https://staging.example.com
Middleware Order (Important!)#
// ✅ Correct order
app.use(cors()); // 1. CORS first
app.use(morgan()); // 2. Logging
app.all("/api/auth/*", authHandler); // 3. Better Auth
app.use(express.json()); // 4. JSON parser (after auth!)
app.use("/api/users", usersRouter); // 5. Routes
app.use(handler404); // 6. 404 handler
app.use(errorHandler); // 7. Error handler (last!)
⚠️ Important: Place express.json() after Better Auth handler, or it will interfere with auth requests!
Router Structure#
Create Feature Routers#
// routes/users.ts
import { Router } from "express";
import type { Request, Response } from "express";
const router = Router();
router.get("/", async (req: Request, res: Response) => {
// Get all users
res.json({ users: [] });
});
router.get("/:id", async (req: Request, res: Response) => {
// Get user by ID
res.json({ user: {} });
});
router.post("/", async (req: Request, res: Response) => {
// Create user
res.status(201).json({ user: {} });
});
export { router as usersRouter };
Register Routers#
// Manually
app.use("/api/users", usersRouter);
app.use("/api/posts", postsRouter);
// Or automatically with subrouters object
const subrouters = {
"/api/users": usersRouter,
"/api/posts": postsRouter,
"/api/comments": commentsRouter,
};
Object.entries(subrouters).forEach(([prefix, router]) => {
app.use(prefix, router);
});
Static File Serving#
import path from "path";
// Serve uploads folder
app.use(
"/uploads",
express.static(path.join(process.cwd(), "uploads"))
);
// Now files are accessible at:
// http://localhost:3000/uploads/profile-pic.jpg
Development Utilities#
Route Printer#
Useful for debugging - prints all registered routes:
printRoutes() {
console.log("\nRoutes registered directly:");
this.app._router?.stack.forEach((layer: any) => {
if (layer.route) {
const path = layer.route.path;
const methods = Object.keys(layer.route.methods)
.map((m) => m.toUpperCase())
.join(", ");
console.log(`${methods.padEnd(10)} ${path}`);
}
});
Object.entries(this.subrouters).forEach(([prefix, router]) => {
console.log(`\nRoutes for prefix: ${prefix}`);
router.stack.forEach((layer: any) => {
if (layer.route) {
const path = prefix + layer.route.path;
const methods = Object.keys(layer.route.methods)
.map((m) => m.toUpperCase())
.join(", ");
console.log(`${methods.padEnd(10)} ${path}`);
}
});
});
}
// Usage
const server = new Server(config);
server.printRoutes();
server.start();
Output:
Routes registered directly:
GET /api/healthcheck
Routes for prefix: /api/users
GET /api/users
GET /api/users/:id
POST /api/users
PUT /api/users/:id
DELETE /api/users/:id
Best Practices#
- ✅ Organize by feature - Group related routes in separate router files
- ✅ Use subrouters - Prevents route duplication and keeps code DRY
- ✅ CORS whitelist - Use environment variables for allowed origins
- ✅ Middleware order matters - Especially auth before JSON parser
- ✅ Error handlers last - 404 and error handlers must be registered last
- ✅ Dev-only routes - Use
if (config.devMode)for testing endpoints - ✅ Health check - Always include
/api/healthcheckfor monitoring
Next Steps#
- Error Handling - Handle errors gracefully
- Authentication - Protect your routes
- Request Validation - Validate incoming requests