Backend Module
Express and NestJS backends with TypeScript, routing, and middleware
Overview
The Backend module generates an Express 5 or a NestJS 12 server with TypeScript. Every Backend API project starts from one of them. The sections below describe Express first.
What You Get
src/
|-- index.ts # Entry point - configures middleware, mounts routes, starts server
|-- env.ts # Validates environment variables at startup
\-- routes/
\-- health.ts # GET /health endpointThe database and auth modules add more files to src/.
Generated Files
src/index.ts
The entry point configures and starts the Express app:
import cors from "cors";
import "dotenv/config";
import express, { Express, NextFunction, Request, Response } from "express";
import helmet from "helmet";
import morgan from "morgan";
import { env } from "./env";
import healthRouter from "./routes/health";
const app: Express = express();
const port = env.PORT;
app.use(helmet());
app.use(cors({ origin: env.CORS_ORIGIN }));
app.use(morgan("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/health", healthRouter);
// 404 handler
app.use((_req: Request, res: Response) => {
res.status(404).json({ error: "Not Found" });
});
// Error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: "Internal Server Error" });
});
const server = app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
// Finish open requests and close the database before exiting
const shutdown = () => server.close(() => process.exit(0));
process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);
export default app;With the auth module, src/index.ts also mounts the auth router at /auth. With Mongoose, the server connects to MongoDB before it starts listening.
On SIGTERM (what Docker and most hosts send to stop a container) or SIGINT (Ctrl+C), the server stops accepting connections, lets open requests finish, closes the database connection, and exits.
src/env.ts
Reads .env and checks every variable the project uses with Zod before the server starts. A missing or malformed value stops the server with a message that names the variable, instead of failing on the first request that needs it. Import env instead of reading process.env:
import { env } from "./env";
console.log(env.PORT); // a number, 3000 unless PORT is setAdd a line to the schema in src/env.ts when you add a variable to .env.
src/routes/health.ts
A GET /health endpoint:
import { Request, Response, Router } from "express";
const router: Router = Router();
router.get("/", (_req: Request, res: Response) => {
res.status(200).json({
status: "ok",
timestamp: new Date().toISOString(),
});
});
export default router;Commands
npm run dev # tsx watch src/index.ts (hot reload, NODE_ENV=development)
npm run build # tsc (compile to dist/)
npm run start # node dist/index.js (NODE_ENV=production)
npm run lint # eslint src
npm run lint:fix # eslint src --fix
npm run format # prettier --write "src/**/*.ts"
npm run type:check # tsc --noEmitWith bun as the package manager,
devrunsbun --watch src/index.tsinstead.
Dependencies
| Package | Role |
|---|---|
express | HTTP framework |
cors | Cross-origin resource sharing |
helmet | Security headers |
morgan | HTTP request logger |
dotenv | Loads .env into process.env |
zod | Validates environment variables in src/env.ts |
cross-env | Sets NODE_ENV in the dev and start scripts |
Environment Variables
PORT=3000
CORS_ORIGIN=*CORS_ORIGIN sets which sites may call the API from a browser. * allows every origin. For production, list the origins of your frontends, separated by commas: CORS_ORIGIN=https://app.example.com,https://admin.example.com.
NODE_ENV is not written to .env. The dev script sets it to development and start sets it to production (through cross-env, so it works on Windows too).
Additional variables are added by the database and auth modules.
Integration with Other Modules
+ Database module
If a database module is selected, the build adds:
- A database client singleton (
src/lib/prisma.tsorsrc/lib/db.ts) - The relevant ORM package and scripts
+ Auth module
If auth is selected, the build adds:
src/lib/jwt.ts- JWT sign/verify utilitiessrc/middleware/requireAuth.ts- Bearer token validation middlewaresrc/services/userStore.ts- User lookup helpersrc/controllers/authController.ts- Register/login handlerssrc/routes/auth.ts- Router mounted at/authJWT_SECRETandJWT_EXPIRES_INenvironment variables
+ Rate limiting
If rate limiting is selected (--rate-limit), the build adds src/middleware/rateLimit.ts with express-rate-limit:
- Every route except
/healthallows 100 requests per client IP in 15 minutes. - With auth,
POST /auth/registerandPOST /auth/loginallow 10 attempts per IP in 15 minutes, which slows down password guessing.
A client over the limit gets 429 Too Many Requests, and limited responses carry RateLimit headers. The limits are counted in memory, so each server instance counts separately. Behind a reverse proxy or load balancer, add app.set("trust proxy", 1) so the limit uses the real client IP.
+ Docker module
If Docker is selected, the build adds Dockerfile, docker-compose.yml, and .dockerignore plus four docker:* scripts.
Extending the Backend
Add a route
- Create
src/routes/users.ts - Implement your route handlers
- Register the router in
src/index.ts:
import usersRouter from "./routes/users";
app.use("/users", usersRouter);Use the requireAuth middleware
import { Router } from "express";
import { requireAuth } from "../middleware/requireAuth";
const router = Router();
router.get("/profile", requireAuth, (req, res) => {
res.json({ message: "Protected route" });
});
export default router;NestJS
Select NestJS in the framework prompt, or pass --framework nestjs. The project uses NestJS 12 with ES modules, so relative imports end in .js.
src/
|-- main.ts # Entry point - loads .env, enables helmet and CORS, starts the server
|-- env.ts # Validates environment variables at startup
|-- app.module.ts # Root module - imports the database and auth modules you selected
\-- health/
\-- health.controller.ts # GET /health endpointScripts
| Script | Command |
|---|---|
dev | nest start --watch with NODE_ENV=development |
build | nest build (output in dist/) |
start | node dist/main.js with NODE_ENV=production |
lint | eslint src |
type:check | tsc --noEmit |
Database modules
| Database | Files | How to use it |
|---|---|---|
| Prisma | src/prisma/prisma.service.ts, src/prisma/prisma.module.ts | Inject PrismaService |
| Drizzle | src/database/database.module.ts, src/database/schema.ts | @Inject(DRIZZLE) private readonly db: Database |
| Mongoose | src/users/user.schema.ts, connection in src/app.module.ts | MongooseModule.forFeature(...) and @InjectModel(User.name) |
The Prisma and Drizzle modules are global, so any module can inject them without importing them.
Auth module
JWT auth adds src/auth/ with the same endpoints as Express: POST /auth/register, POST /auth/login, and GET /auth/me. Request bodies are validated with Zod.
With rate limiting (--rate-limit), the build adds @nestjs/throttler: a global guard allows 100 requests per client IP in 15 minutes, and register and login allow 10. Change the limits in src/app.module.ts and src/auth/auth.controller.ts.
src/env.ts and CORS_ORIGIN work as they do for Express.
Protect a route with the guard:
import { Controller, Get, Req, UseGuards } from "@nestjs/common";
import { type AuthenticatedRequest, AuthGuard } from "../auth/auth.guard.js";
@Controller("profile")
export class ProfileController {
@Get()
@UseGuards(AuthGuard)
getProfile(@Req() request: AuthenticatedRequest) {
return { user: request.user };
}
}