Understanding a NestJS Authentication App for an Express.js Developer

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, }); co

Mar 27, 2025 - 13:54
 0
Understanding a NestJS Authentication App for an Express.js Developer

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.

Dive In

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