Home

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 configuration

Entry Point

The server initializes in src/index.ts.
It sets up global middleware, connects to the database, and mounts the API routes.

Key Configuration

OptionValue
Port3001 (or process.env.PORT)
CORSCredentials enabled, origin restricted to process.env.APP_ORIGIN
Route PrefixAll 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 KindStatusResponse Body
ZodError400{ issues: …ZodError.flatten() }
AppError(code you set){ message, statusCode }
Everything else500{ 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);

On this page