API Express Overview
Express structure in create-light-stack.
Project Structure
Code is organised inside src for clear separation of concerns.
src/
├── controllers/ # Request handlers (business logic)
├── middlewares/ # Interceptors (Logs, Auth, Errors)
├── routes/ # Route definitions and method mapping
├── utils/ # Shared utilities (AppError)
└── index.ts # Entry point and server configurationEntry Point
The server initializes in src/index.ts.
It sets up global middleware, connects to the database, and mounts the API routes.
Key Configuration
| Option | Value |
|---|---|
| Port | 3001 (or process.env.PORT) |
| CORS | Credentials enabled, origin restricted to process.env.APP_ORIGIN |
| Route Prefix | All routes mounted under /api |
app.use(express.json());
app.use(
cors({ origin: process.env.APP_ORIGIN, credentials: true })
);
// All routes live under /api
app.use("/api", routes);
// Global error handler MUST be last
app.use(errorMiddleware);Routing
Routes are defined in src/routes and delegate to handler functions in src/controllers.
Defining Routes
Use a standard express.Router instance:
// src/routes/example.routes.ts
import express from 'express';
import { ExampleController } from '../controllers/example.controller.js';
const router = express.Router();
router.get('/', ExampleController);
export default router;Controllers
Controllers are async functions that contain request-level logic.
//src/controllers/example.controller.ts
import { Request, Response, NextFunction } from "express";
import { AppError } from "../utils/AppError.js";
export const ExampleController = async (
_req: Request,
res: Response,
next: NextFunction,
) => {
try {
return res.status(200).json({
status: "success",
message: "This is the example route",
});
} catch (err) {
next(err);
}
};Validation
The database package exposes Zod validation schemas derived from your database models. These schemas ensure type-safe and validated inputs in your Express controllers.
// Drizzle
// src/controllers/example.controller.ts
import { db, example, insertExampleSchema } from "@light/database";
export const createExample = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const validData = insertExampleSchema.parse(req.body);
const [newItem] = await db.insert(example)
.values(validData)
.returning();
res.status(201).json({ status: "success", data: newItem });
} catch (err) {
next(err);
}
};// Mongoose
// src/controllers/example.controller.ts
import type { Request, Response, NextFunction } from "express";
import { example, insertExampleSchema } from "@light/database";
import { AppError } from "../utils/AppError.js";
export const createExample = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const validData = insertExampleSchema.parse(req.body);
const newDoc = await example.create(validData);
res.status(201).json({ status: "success", data: newDoc });
} catch (err) {
next(err);
}
};Error Handling
A single errorMiddleware (src/middlewares/error.middleware.ts) normalizes every failure that reaches it, guaranteeing a consistent error shape for the client.
The Error Middleware
| Error Kind | Status | Response Body |
|---|---|---|
| ZodError | 400 | { issues: …ZodError.flatten() } |
| AppError | (code you set) | { message, statusCode } |
| Everything else | 500 | { message: 'Internal server error' } (no stack leak) |
Using AppError
Any route, controller, or utility can throw an AppError and the middleware will reply with the supplied status code.
import { AppError } from '../utils/AppError.js';
// 404 Not Found
throw new AppError('Resource not found', 404);
// 403 Forbidden
throw new AppError('You do not have permission', 403);