A simple demonstration backend app that consists of:
- π Authentication (Auth)
- π§βπ Users
- β Tasks
The project is written in TypeScript, uses Express 5, TypeORM, PostgreSQL, Zod for validation, and follows a clean modular structure with Dependency Injection via module composers.
Prerequisites:
- Node.js v22.19.0
- Docker + Docker Compose
Steps:
- Create or request a .env file with required variables (see env.example)
- Start infrastructure in Docker:
docker compose up - Install dependencies:
npm i - Run the app:
- Dev:
npm run start:dev - Prod:
npm run start:prod
- Dev:
Useful scripts:
- Build:
npm run build - Lint:
npm run lint/npm run lint:fix
Default ports:
- App:
APP_PORT=5001 - Postgres: exposed on host as
DB_PORTfrom.env(mapped to container 5432)
- π’ Node.js 22.19.0
- β‘ Express 5
- ποΈ TypeORM
- π PostgreSQL
- π§© Zod (validation)
- π JWT (auth)
- π§ͺ TypeScript with strict mode and verbatimModuleSyntax
- π§° ESLint + Prettier config
- π³ Docker Compose for Postgres
Top-level directories youβll interact with most:
appβ application bootstrap and global composition (things not reused by modules)infrastructureβ infrastructural services (ConfigService, Database, Logger, etc.)sharedβ reusable building blocks (types, utils, middlewares, schemas)modulesβ main feature modules where most business logic livesambientβ ambient type augmentations (e.g., Express Request extension)
Core feature modules:
authusertask
- Repository pattern: all ORM/database queries are isolated inside Repository classes. Only repositories talk to TypeORM.
- Services expose business logic and use repositories to access the database.
- Controllers interact with services.
- Validation is done with Zod via middlewares. Controllers receive already validated and typed data.
- Dependency Injection is realized via per-module composers + a global composer.
- Validation: Zod + middlewares.
- If there is a Zod schema, the TypeScript type must be inferred from it via
z.infer. - If no Zod schema exists, use regular TypeScript types.
Example:
import { z } from 'zod';
export const SignInSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export type SignInDto = z.infer<typeof SignInSchema>;The Express Request type is augmented globally (see src/ambient/express.d.ts) with:
user?: ActiveUserβ current user injected by auth middlewarevalidated?: unknownβ storage for validated, transformed, typed data
Controllers must use the custom TypedRequest instead of express.Request to get strong typing for validated:
import type { Response } from 'express';
import type { TypedRequest } from '@types';
export class TaskController {
findOne = async (
req: TypedRequest<{ params: { id: number } }>,
res: Response,
) => {
const user = req.user!;
const taskId = req.validated.params.id;
// ...
res.status(200).json({});
};
}Rules for imports:
- Within a module β use only relative imports.
- Between modules β use absolute imports via
tsconfig.pathsaliases. - Do not import deep internals of another module; import only from the module barrel (its
index.ts).
Bad:
// β Deep import from another module (forbidden)
import { UserEntity } from "@modules/user/entities/user.entity.js";Good:
// β
Only from the module API (barrel)
import { UserEntity } from "@modules/user";Barrel files (re-export index.ts):
- Exist only at the root of a module to expose its public API.
- Inside a module, do not re-export; import directly from files using relative paths.
Path aliases (see tsconfig.json):
@modules/*,@infrastructure/*,@app/*,@schemas/*,@utils/*,@ambient/*,@types.
The project enforces TypeScriptβs verbatimModuleSyntax: true. If something is used only as a type β import it as a type.
Bad:
// β Runtime import when only types are needed
import { ConfigService } from '@infrastructure/config-service';Good:
// β
Type-only import
import type { ConfigService } from '@infrastructure/config-service';Youβll see this pattern across the codebase (e.g., controllers and services use import type {...} where appropriate).
Access to environment variables is centralized via ConfigService (src/infrastructure/config-service).
- It validates
.envusing Zod (env.schema.ts) and exposes a typedenvobject. - When you change
.env, you must update both validation (env.schema.ts) and typing (seetypes.ts/environment.d.tsin the same folder).
env variables (see env.example):
- APP_PORT, APP_ENV
- DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_DATABASE
- JWT_SECRET, JWT_SALT_ROUNDS, ACCESS_TOKEN_TTL, REFRESH_TOKEN_TTL
Example usage:
const port = configService.env.APP_PORT; // number (coerced and validated)Each module has its own composer that:
- Initializes and wires internal dependencies (repositories, services, controllers)
- Builds the module router
- Returns the router and other instances needed by other modules
Thereβs a global composer in app that:
- Initializes infrastructural services (Logger, Config, Database)
- Runs module composers
- Returns composed module routers and cross-cutting instances (e.g., access token guard)
Example (simplified):
// app/composers/modules.composer.ts
const user = runUserModuleComposer({ dataSource });
const auth = runAuthModuleComposer({ dataSource, configService, userService: user.userService });
const task = runTaskModuleComposer({ dataSource });
return { moduleRouters: { userRouter: user.userRouter, authRouter: auth.authRouter, taskRouter: task.taskRouter }, accessTokenGuard: auth.accessTokenGuard };Routing aggregation:
// app/composers/routers.composer.ts
rootRouter.use('/auth', moduleRouters.authRouter);
rootRouter.use('/users', [accessTokenGuard.canActivate], moduleRouters.userRouter);
rootRouter.use('/tasks', [accessTokenGuard.canActivate], moduleRouters.taskRouter);- Repository: isolates TypeORM and database queries.
- Service: uses repositories to implement business logic and DB access.
- Controller: uses services; no direct ORM calls.
A PostgreSQL instance is provided via Docker Compose. The service reads credentials from your .env and exposes Postgres on ${DB_PORT}:5432.
services:
postgres:
image: postgres:17.5
ports:
- "${DB_PORT}:5432"
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE}
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/dataISC
- This is a small demo application focused on structure and conventions.
- Feel free to extend modules or add new ones following the same patterns.