DEV Community

Cover image for Understanding a NestJS Authentication App for an Express.js Developer
FredAbod
FredAbod

Posted on

3

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'));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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';
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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

Bye

Top comments (0)