NestJS has become my go-to framework for building production-ready backend services. Its opinionated structure, native TypeScript support, and Angular-inspired module system make it incredibly powerful for teams. In this post I'll walk through the architectural decisions I make when scaffolding a new NestJS + TypeORM project.
Why NestJS?
Express is flexible โ sometimes too flexible. Without conventions, large codebases devolve into spaghetti. NestJS gives you dependency injection, decorators, modules, and a clear layered architecture out of the box. Every controller, service, and repository knows exactly where it lives.
Tip
NestJS is fully compatible with Express under the hood. You can drop down to raw Express when you need to, but you rarely will.
Project Structure
I organise by domain, not by type. Each feature (e.g. users, products, orders) gets its own module folder:
src/
โโโ users/
โ โโโ users.module.ts
โ โโโ users.controller.ts
โ โโโ users.service.ts
โ โโโ users.entity.ts
โ โโโ dto/
โ โโโ create-user.dto.ts
โ โโโ update-user.dto.ts
โโโ auth/
โ โโโ auth.module.ts
โ โโโ auth.service.ts
โ โโโ jwt.strategy.ts
โ โโโ guards/
โ โโโ jwt-auth.guard.ts
โโโ app.module.tsSetting Up TypeORM
Install the required packages and configure the TypeORM module in AppModule. I always use environment variables for credentials and enable synchronize only in development:
// app.module.ts
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
username: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production',
}),
UsersModule,
AuthModule,
],
})
export class AppModule {}Defining Entities
Entities are TypeScript classes decorated with @Entity(). I always extend a BaseEntity class that holds common columns to keep things DRY:
// base.entity.ts
export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// users/users.entity.ts
@Entity('users')
export class User extends BaseEntity {
@Column({ unique: true })
email: string;
@Column()
passwordHash: string;
@Column({ default: 'user' })
role: string;
}Guards & Role-Based Access
NestJS guards are the cleanest way to protect routes. Here's a minimal JWT guard that also applies role checks via a custom @Roles() decorator:
// guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.get<string[]>('roles', context.getHandler());
if (!required) return true;
const { user } = context.switchToHttp().getRequest();
return required.includes(user.role);
}
}
// Then on your controller:
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}Interceptors for Response Transformation
I use a global response interceptor to wrap every API response in a consistent shape, making frontend integration predictable:
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
);
}
}
// Register globally in main.ts:
app.useGlobalInterceptors(new TransformInterceptor());Key Takeaways
- Organise by domain, not by layer โ every feature is self-contained
- Use environment variables for all config; never hardcode credentials
- Synchronize: true is only for development โ use migrations in production
- Guards + Reflector metadata is the cleanest RBAC pattern in NestJS
- A global interceptor keeps API responses uniform across all endpoints
Note
All code samples in this post are simplified for clarity. In a real project you'd also add validation pipes (class-validator), exception filters, and a Swagger module for automatic API documentation.