Understanding a NestJS Authentication App for an Express.js Developer
If you're coming from the world of Express.js, where everything is a bit more "manual" and you have to wire up routes, middleware, and controllers yourself, NestJS might feel like stepping into a world where everything is pre-organized for you. Think of it as moving from a DIY furniture kit to a fully furnished apartment. Let’s break down this NestJS authentication app and compare it to how you’d typically build a basic CRUD app in Express.js.
The Entry Point
Express.js:
In Express, you’d typically start with an app.js
or server.js
file where you initialize the app, set up middleware, and define routes.
const express = require('express');
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => console.log('Server running on port 3000'));
NestJS:
In NestJS, the entry point is main.ts
. It’s where the app is bootstrapped, and global configurations like middleware or pipes are applied.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Comparison:
- In Express, you manually set up middleware like
body-parser
. - In NestJS, you use decorators and global pipes like
ValidationPipe
to handle validation and transformation automatically.
Routing
Express.js:
In Express, you define routes directly in your app.js
or split them into route files.
const express = require('express');
const router = express.Router();
router.get('/users', (req, res) => {
res.send('Get all users');
});
router.post('/users', (req, res) => {
res.send('Create a user');
});
module.exports = router;
NestJS:
In NestJS, routes are defined in controllers using decorators like @Controller
, @Get
, and @Post
.
import { Controller, Get, Post, Body } from '@nestjs/common';
@Controller('users') // Base route: /users
export class UsersController {
@Get()
getAllUsers() {
return 'Get all users';
}
@Post()
createUser(@Body() body: any) {
return `Create a user with data: ${JSON.stringify(body)}`;
}
}
Comparison:
- In Express, routes are functions tied to HTTP methods (
app.get
,app.post
). - In NestJS, routes are methods in a class, and decorators define the HTTP method and path.
Middleware
Express.js:
Middleware in Express is a function that processes requests before they reach the route handler.
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
app.use(logger);
NestJS:
Middleware in NestJS is similar but is implemented as a class or function and applied globally or to specific routes.
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log(`${req.method} ${req.url}`);
next();
}
}
Comparison:
- In Express, middleware is just a function.
- In NestJS, middleware can be a class, giving it more structure and reusability.
Controllers and Services
Express.js:
In Express, you might handle everything in the route handler itself or split logic into separate files.
app.post('/auth/register', async (req, res) => {
const { username, password } = req.body;
// Hash password, save user to DB, etc.
res.send('User registered');
});
NestJS:
In NestJS, controllers handle the routes, but the actual logic is moved to services for better separation of concerns.
// auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
async register(@Body() body: any) {
return this.authService.register(body.username, body.password);
}
}
// auth.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
async register(username: string, password: string) {
// Hash password, save user to DB, etc.
return 'User registered';
}
}
Comparison:
- In Express, you might mix route handling and business logic in the same file.
- In NestJS, controllers handle routing, and services handle business logic, making the code more modular and testable.
Database Integration
Express.js:
In Express, you’d use an ORM like Mongoose directly in your route handlers or a separate model file.
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: String,
password: String,
});
const User = mongoose.model('User', UserSchema);
app.post('/auth/register', async (req, res) => {
const user = new User(req.body);
await user.save();
res.send('User registered');
});
NestJS:
In NestJS, you use @nestjs/mongoose to define schemas and inject models into services.
// user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
@Schema()
export class User extends Document {
@Prop({ required: true })
username: string;
@Prop({ required: true })
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './schemas/user.schema';
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<User>) {}
async create(username: string, password: string) {
const user = new this.userModel({ username, password });
return user.save();
}
}
Comparison:
- In Express, you directly use Mongoose models in your route handlers.
- In NestJS, models are injected into services, keeping the code cleaner and more testable.
Authentication
Express.js:
In Express, you’d use middleware like passport
or manually validate JWT tokens.
const jwt = require('jsonwebtoken');
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).send('Unauthorized');
try {
const decoded = jwt.verify(token, 'secret');
req.user = decoded;
next();
} catch {
res.status(401).send('Unauthorized');
}
};
app.get('/protected', authMiddleware, (req, res) => {
res.send(`Hello ${req.user.username}`);
});
NestJS:
In NestJS, you use guards and strategies to handle authentication.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secret',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
// jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// auth.controller.ts
@UseGuards(JwtAuthGuard)
@Get('protected')
getProtected(@Request() req) {
return `Hello ${req.user.username}`;
}
Comparison:
- In Express, you manually validate tokens in middleware.
- In NestJS, guards and strategies handle authentication, making it more reusable and declarative.
Validation
Express.js:
In Express, you’d use a library like joi
or manually validate inputs.
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().required(),
password: Joi.string().min(8).required(),
});
app.post('/auth/register', (req, res) => {
const { error } = schema.validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
res.send('User registered');
});
NestJS:
In NestJS, validation is built-in using class-validator
and class-transformer
.
import { IsString, MinLength } from 'class-validator';
export class RegisterDto {
@IsString()
username: string;
@IsString()
@MinLength(8)
password: string;
}
// auth.controller.ts
@Post('register')
async register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto.username, registerDto.password);
}
Comparison:
- In Express, you manually validate inputs using libraries.
- In NestJS, validation is declarative and tied to DTOs (Data Transfer Objects).
Final Thoughts 😊
NestJS is like Express.js on steroids. It takes care of a lot of boilerplate and enforces a clean, modular structure. While Express gives you the freedom to do things your way, NestJS provides a framework that guides you toward best practices.
If Express is a blank canvas, NestJS is a paint-by-numbers kit. Both let you create beautiful art, but NestJS ensures you stay within the lines. And hey, who doesn’t like a little structure in their life? 🎨😉😂😉🎨
You can check out my article on Building a NestJS Authentication API with MongoDB and JWT: A Step-by-Step Guide
Top comments (0)