The Node.js Event Loop: What You Actually Need to Know
A practical, no-nonsense breakdown of the event loop, the phases that matter for backend developers, and how blocking it destroys your API's throughput.
How I structure production-grade APIs with authentication, RBAC, validation, and clean module boundaries baked in from day one.
After building APIs across multiple stacks — Express, FastAPI, Django — NestJS has been my go-to for TypeScript backends that need to scale with a team. The opinionated structure eliminates the "where does this go?" decision fatigue that kills velocity in growing codebases.
The key insight: structure is a feature. When a new engineer joins your team, a well-structured NestJS project is self-documenting.
Before writing a single line of logic, I define module boundaries. Each domain gets its own module:
src/
├── auth/
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ └── strategies/
├── users/
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── dto/
└── app.module.ts
This prevents the spaghetti-import problem that kills Express projects at scale.
I use JWT with a refresh token rotation strategy. The guard pattern in NestJS makes it trivially easy to apply auth globally and override per-route:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
Apply globally in app.module.ts, then use @Public() decorator to opt specific routes out. This is safer than the reverse — everything is protected by default.
RBAC is a cross-cutting concern, so I implement it as a custom decorator + guard pair:
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
Every DTO gets full validation. No raw request body ever touches service logic:
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
@Matches(/^(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase and number',
})
password: string;
@IsEnum(Role)
role: Role;
}
Enable ValidationPipe globally with whitelist: true to strip unknown properties automatically.
The biggest lesson from building the National Buying Consortium platform: invest in the data layer early. A clean NestJS architecture cannot save you from a poorly normalized schema. TypeORM migrations + careful schema design upfront saved us months of painful refactoring.
When in doubt, add another module. Over-modularization is easier to fix than under-modularization.