Build a Secure Login and Signup API in NestJS
This final lesson doesn't introduce new concepts; it integrates everything from this module, JWT signing and verification, Passport strategies, bcrypt hashing, RBAC, and route protection patterns, into one complete, working authentication module you could genuinely ship in a real project.
By the end of this lesson, you'll have a full signup and login API, and a hands-on exercise extending it with an admin-only protected route, tying together the RBAC pattern from earlier in this module with everything else you've built.
Secure Login Signup API NestJS: Learning Objectives
- Integrate DTO validation, bcrypt hashing, and JWT signing into a complete signup flow.
- Build a login endpoint that verifies hashed passwords and issues a JWT.
- Wire together JwtStrategy, JwtAuthGuard, and the @Public() exemption pattern from this module.
- Assemble a complete, cohesive AuthModule ready to plug into a larger application.
- Extend the module with an admin-only route as a practical exercise applying RBAC.
Secure Login Signup API NestJS: Key Terms and Definitions
- Auth module: A self-contained NestJS module bundling together all authentication-related controllers, services, strategies, and guards for an application.
- SignupDto / LoginDto: DTOs defining and validating the exact shape of data expected on registration and login requests.
- End-to-end auth flow: The complete journey from a user submitting a signup form through to making authenticated requests using an issued JWT.
- Integration lesson: A lesson that combines previously taught individual concepts into one cohesive, working feature rather than introducing new theory.
How Secure Login Signup API NestJS Works: Detailed Explanation
A production-ready auth module brings together every piece covered across this module into one cohesive flow. The signup endpoint accepts a validated SignupDto (using class-validator decorators from Module 2), checks whether the email is already registered, hashes the submitted password using bcrypt (Lesson 6) with an appropriate salt rounds value, and stores the new user record with only the hash, never the plain-text password.
The login endpoint accepts a validated LoginDto, looks up the user by email, and uses bcrypt.compare() to verify the submitted password against the stored hash. If valid, it signs a JWT (Lesson 1) containing the user's ID, email, and role as claims, returning this token to the client. From this point forward, every protected route in the application relies on the JwtStrategy and JwtAuthGuard built in Lessons 1, 2, and 8, extracting and verifying this token automatically via Passport, with a global guard protecting routes by default and a @Public() decorator explicitly exempting the signup and login routes themselves, since requiring authentication to reach the very endpoints that grant it would be a contradiction.
Role information, included in the JWT payload at login time, flows naturally into the RBAC system from Lesson 3: once JwtStrategy's validate() method returns a user object including their role, any route decorated with both the authentication guard and RolesGuard, plus a specific @Roles('admin') requirement, can restrict access precisely to users holding that role, completing the full authentication-to-authorization pipeline this module has built up piece by piece.
As a practical exercise extending this complete module, consider adding a dedicated admin-only route, such as GET /admin/users, that returns a list of all registered users, but only to authenticated requests where the user's role is 'admin'. Implementing this exercise requires nothing new: it's a direct application of @UseGuards(JwtAuthGuard, RolesGuard) combined with @Roles('admin') on a new controller method, proving that once this foundational auth module is built correctly, extending it with new protected, role-restricted functionality becomes almost entirely declarative.
Interview-Friendly Explanation
A strong interview or viva answer for secure login signup api nestjs must go beyond a one-line definition. Interviewers evaluating BCA, MCA, B.Tech, and self-taught backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters for securing production Node.js applications. Use the four-point framework below when answering under time pressure.
- Definition point: Auth module: A self-contained NestJS module bundling together all authentication-related controllers, services, strategies, and guards for an application.
- Working point: Build a login endpoint that verifies hashed passwords and issues a JWT.
- Example point: This exact combination, DTO validation, bcrypt hashing, JWT issuance, and guard-based protection, forms the backbone of the authentication module in the overwhelming majority of real-world NestJS applications shipped to production.
- Conclusion point: A production-ready auth module is really just the disciplined combination of every individual piece covered throughout this module.
How to Answer This in a Technical Interview
- Give a two-to-three sentence definition using correct authentication and security terminology.
- Add one specific example drawn from a real backend scenario such as a banking, SaaS, or e-commerce login flow.
- Mention any security trade-offs (e.g. JWT vs sessions, access vs refresh tokens) since interviewers often probe this contrast.
- Close with one benefit, limitation, or production security consideration.
- Avoid vague answers like "it just checks if the user is logged in" — interviewers filter these out immediately.
Practical Scenario
Imagine you are securing a production SaaS backend, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make while protecting that backend: how users prove who they are, how long that proof stays valid, who is allowed to do what, and how you defend against abuse.
Secure Login Signup API NestJS: Architecture and Flow Diagram
Visualize the complete, integrated auth flow:
[POST /auth/signup] --(validated SignupDto)--> [Hash password with bcrypt] --> [Store user with role] --> [Return created user (no password)]
[POST /auth/login] --(validated LoginDto)--> [bcrypt.compare() verifies password] --> [Sign JWT with sub, email, role claims] --> [Return accessToken]
[GET /profile] --(Authorization: Bearer <token>)--> [JwtAuthGuard + JwtStrategy verify token] --> [req.user populated] --> [Route executes]
[GET /admin/users] --(same token)--> [JwtAuthGuard + RolesGuard + @Roles('admin')] --> [Only admins allowed] --> [List returned or 403 Forbidden]
Secure Login Signup API NestJS: Module Concept Reference Table
| Module Concept | Where It's Used in This Integrated Flow |
|---|---|
| DTO validation (Module 2) | SignupDto and LoginDto validate incoming request shape and rules |
| bcrypt hashing (Lesson 6) | Passwords hashed at signup, verified at login via bcrypt.compare() |
| JWT signing (Lesson 1) | Login issues a JWT containing sub, email, and role claims |
| Passport strategy (Lesson 2) | JwtStrategy verifies tokens and populates request.user |
| Global guard + @Public() (Lesson 8) | Signup/login routes exempted; everything else protected by default |
| RBAC (Lesson 3) | @Roles('admin') restricts the exercise's admin-only route |
Secure Login Signup API NestJS: NestJS Code Example
// dto/signup.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class SignupDto {
@IsString() @MinLength(2)
name: string;
@IsEmail()
email: string;
@IsString() @MinLength(8)
password: string;
}
// dto/login.dto.ts
import { IsEmail, IsString } from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
password: string;
}
// auth.service.ts — the complete signup + login flow
import { Injectable, ConflictException, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
interface StoredUser {
id: number;
name: string;
email: string;
passwordHash: string;
role: 'user' | 'admin';
}
@Injectable()
export class AuthService {
private users: StoredUser[] = [];
constructor(private readonly jwtService: JwtService) {}
async signup(dto: SignupDto) {
const existing = this.users.find((u) => u.email === dto.email);
if (existing) throw new ConflictException('Email already registered');
const passwordHash = await bcrypt.hash(dto.password, 10);
const newUser: StoredUser = {
id: this.users.length + 1,
name: dto.name,
email: dto.email,
passwordHash,
role: 'user', // default role for every new signup
};
this.users.push(newUser);
return { id: newUser.id, name: newUser.name, email: newUser.email };
}
async login(dto: LoginDto) {
const user = this.users.find((u) => u.email === dto.email);
if (!user) throw new UnauthorizedException('Invalid credentials');
const passwordMatches = await bcrypt.compare(dto.password, user.passwordHash);
if (!passwordMatches) throw new UnauthorizedException('Invalid credentials');
const accessToken = await this.jwtService.signAsync({
sub: user.id,
email: user.email,
role: user.role,
});
return { accessToken };
}
// Exercise: supports the admin-only route below
findAllUsers() {
return this.users.map(({ id, name, email, role }) => ({ id, name, email, role }));
}
}
// auth.controller.ts — wiring signup, login, and the admin exercise route together
import { Controller, Post, Get, Body, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignupDto } from './dto/signup.dto';
import { LoginDto } from './dto/login.dto';
import { Public } from './public.decorator';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('signup')
signup(@Body() dto: SignupDto) {
return this.authService.signup(dto);
}
@Public()
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
// Exercise: an admin-only protected route
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get('admin/users')
listAllUsers() {
return this.authService.findAllUsers();
}
}
signup() hashes the incoming password with bcrypt and stores a new user with a default 'user' role, never persisting the plain-text password. login() verifies the submitted password against the stored hash using bcrypt.compare(), and on success signs a JWT carrying the user's ID, email, and role as claims, exactly the payload shape RolesGuard needs downstream. In the controller, both signup and login are marked @Public(), correctly exempting them from the application's global authentication guard, since requiring a token to reach the endpoints that issue one would be circular. The final listAllUsers() route is the hands-on exercise: it applies both JwtAuthGuard (authentication) and RolesGuard (authorization) together with @Roles('admin'), meaning only a logged-in user whose JWT carries the 'admin' role can successfully call GET /auth/admin/users, directly reusing the RBAC pattern from Lesson 3 without introducing a single new concept.
Real-World Secure Login Signup API NestJS Industry Examples
- This exact combination, DTO validation, bcrypt hashing, JWT issuance, and guard-based protection, forms the backbone of the authentication module in the overwhelming majority of real-world NestJS applications shipped to production.
- Admin-only endpoints like the listAllUsers() exercise route are an extremely common real-world requirement, appearing in nearly every application that distinguishes regular users from staff or administrators.
- Starter templates and boilerplates for NestJS SaaS products almost always ship with a pre-built auth module resembling this exact structure, since reimplementing it from scratch for every new project would be wasteful.
- Take-home interview assignments for backend NestJS roles frequently ask candidates to build precisely this kind of signup/login/protected-route flow, since it demonstrates fluency across validation, security, and authorization in one exercise.
Secure Login Signup API NestJS Interview Questions and Answers
Q1. Walk through the complete flow of a user signing up and then successfully calling a protected route.
Short answer: The user submits a validated SignupDto; the server checks for an existing account, hashes the password with bcrypt, and stores the new user. On login, the server verifies the submitted password against the stored hash using bcrypt.compare() and, if valid, signs a JWT containing the user's ID, email, and role. The client then includes this JWT on subsequent requests, where JwtAuthGuard and JwtStrategy verify it and populate request.user, allowing protected routes to execute.
Detailed explanation: A production-ready auth module brings together every piece covered across this module into one cohesive flow. The signup endpoint accepts a validated SignupDto (using class-validator decorators from Module 2), checks whether the email is already registered, hashes the submitted password using bcrypt (Lesson 6) with an appropriate salt rounds value, and stores the new user record with only the hash, never the plain-text password. The login endpoint accepts a validated LoginDto, looks up the user by email, and uses bcrypt.compare() to verify the submitted password against the stored hash. If valid, it signs a JWT (Lesson 1) containing the user's ID, email, and role as claims, returning this token to the client. From this point forward, every protected route in the application relies on the JwtStrategy and JwtAuthGuard built in Lessons 1, 2, and 8, extracting and verifying this token automatically via Passport, with a global guard protecting routes by default and a @Public() decorator explicitly exempting the signup and login routes themselves, since requiring authentication to reach the very endpoints that grant it would be a contradiction. Role information, included in the JWT payload at login time, flows naturally into the RBAC system from Lesson 3: once JwtStrategy's validate() method returns a user object including their role, any route decorated with both the authentication guard and RolesGuard, plus a specific @Roles('admin') requirement, can restrict access precisely to users holding that role, completing the full authentication-to-authorization pipeline this module has built up piece by piece. As a practical exercise extending this complete module, consider adding a dedicated admin-only route, such as GET /admin/users, that returns a list of all registered users, but only to authenticated requests where the user's role is 'admin'. Implementing this exercise requires nothing new: it's a direct application of @UseGuards(JwtAuthGuard, RolesGuard) combined with @Roles('admin') on a new controller method, proving that once this foundational auth module is built correctly, extending it with new protected, role-restricted functionality becomes almost entirely declarative.
Practical example: This exact combination, DTO validation, bcrypt hashing, JWT issuance, and guard-based protection, forms the backbone of the authentication module in the overwhelming majority of real-world NestJS applications shipped to production.
Interview tip: Complete auth flow: validated DTOs → bcrypt hashing at signup → bcrypt.compare() at login → JWT issuance with sub/email/role claims.
Revision hook: A production-ready auth module is really just the disciplined combination of every individual piece covered throughout this module.
Q2. Why are the signup and login routes marked @Public() even though the rest of the application uses a global authentication guard?
Short answer: Since a global guard protects every route by default, signup and login must be explicitly exempted using @Public(), because requiring a valid JWT to reach the very endpoints responsible for creating an account or issuing that JWT in the first place would be a logical contradiction, permanently locking out any new or logging-in user.
Detailed explanation: signup() hashes the incoming password with bcrypt and stores a new user with a default 'user' role, never persisting the plain-text password. login() verifies the submitted password against the stored hash using bcrypt.compare(), and on success signs a JWT carrying the user's ID, email, and role as claims, exactly the payload shape RolesGuard needs downstream. In the controller, both signup and login are marked @Public(), correctly exempting them from the application's global authentication guard, since requiring a token to reach the endpoints that issue one would be circular. The final listAllUsers() route is the hands-on exercise: it applies both JwtAuthGuard (authentication) and RolesGuard (authorization) together with @Roles('admin'), meaning only a logged-in user whose JWT carries the 'admin' role can successfully call GET /auth/admin/users, directly reusing the RBAC pattern from Lesson 3 without introducing a single new concept.
Practical example: Admin-only endpoints like the listAllUsers() exercise route are an extremely common real-world requirement, appearing in nearly every application that distinguishes regular users from staff or administrators.
Interview tip: Signup and login routes must be @Public(); everything else protected by a global JwtAuthGuard.
Revision hook: Getting the @Public() exemption right on signup/login is a small detail with an outsized impact if missed.
Q3. How does a user's role, set at signup, eventually restrict access to an admin-only route?
Short answer: The role is stored with the user record at signup and included as a claim in the JWT payload during login. When JwtStrategy's validate() method returns this role as part of req.user, RolesGuard can later compare it against a route's @Roles('admin') requirement, allowing or denying access based on whether the authenticated user's role matches.
Detailed explanation: This lesson integrates every concept from Module 4 into one complete, working authentication module: validated SignupDto and LoginDto classes enforce request shape, bcrypt hashes passwords at signup and verifies them at login, a signed JWT carrying the user's ID, email, and role is issued on successful login, and a global JwtAuthGuard combined with a @Public() decorator ensures signup and login remain accessible while every other route stays protected by default. The role embedded in the JWT payload flows directly into the RBAC system from earlier in this module, demonstrated through a hands-on exercise: an admin-only GET /auth/admin/users route protected by both JwtAuthGuard and RolesGuard with @Roles('admin'). This complete, integrated example is exactly the shape of authentication module you would build and extend in a real production NestJS application.
Practical example: Starter templates and boilerplates for NestJS SaaS products almost always ship with a pre-built auth module resembling this exact structure, since reimplementing it from scratch for every new project would be wasteful.
Interview tip: Role assigned at signup flows into the JWT payload, consumed later by RolesGuard for RBAC enforcement.
Revision hook: Embedding role information in the JWT payload at login time is what makes RBAC enforcement downstream effortless.
Q4. What would you need to change to add a new admin-only feature to this existing auth module?
Short answer: Very little: you would simply add a new controller method, apply @UseGuards(JwtAuthGuard, RolesGuard) and @Roles('admin') to it, exactly as shown in the listAllUsers() exercise, since the entire authentication and role-checking infrastructure is already in place and fully reusable for any new protected, role-restricted route.
Detailed explanation: A production-ready auth module brings together every piece covered across this module into one cohesive flow. The signup endpoint accepts a validated SignupDto (using class-validator decorators from Module 2), checks whether the email is already registered, hashes the submitted password using bcrypt (Lesson 6) with an appropriate salt rounds value, and stores the new user record with only the hash, never the plain-text password. The login endpoint accepts a validated LoginDto, looks up the user by email, and uses bcrypt.compare() to verify the submitted password against the stored hash. If valid, it signs a JWT (Lesson 1) containing the user's ID, email, and role as claims, returning this token to the client. From this point forward, every protected route in the application relies on the JwtStrategy and JwtAuthGuard built in Lessons 1, 2, and 8, extracting and verifying this token automatically via Passport, with a global guard protecting routes by default and a @Public() decorator explicitly exempting the signup and login routes themselves, since requiring authentication to reach the very endpoints that grant it would be a contradiction. Role information, included in the JWT payload at login time, flows naturally into the RBAC system from Lesson 3: once JwtStrategy's validate() method returns a user object including their role, any route decorated with both the authentication guard and RolesGuard, plus a specific @Roles('admin') requirement, can restrict access precisely to users holding that role, completing the full authentication-to-authorization pipeline this module has built up piece by piece. As a practical exercise extending this complete module, consider adding a dedicated admin-only route, such as GET /admin/users, that returns a list of all registered users, but only to authenticated requests where the user's role is 'admin'. Implementing this exercise requires nothing new: it's a direct application of @UseGuards(JwtAuthGuard, RolesGuard) combined with @Roles('admin') on a new controller method, proving that once this foundational auth module is built correctly, extending it with new protected, role-restricted functionality becomes almost entirely declarative.
Practical example: Take-home interview assignments for backend NestJS roles frequently ask candidates to build precisely this kind of signup/login/protected-route flow, since it demonstrates fluency across validation, security, and authorization in one exercise.
Interview tip: Adding new protected, role-restricted routes to an existing auth module is largely declarative once the foundation is built.
Revision hook: Once this foundation exists, extending an application with new protected or role-restricted features requires very little additional code.
Secure Login Signup API NestJS MCQs and Practice Questions
1. In the complete signup flow, what should be stored in the database for a user's password?
- The plain-text password
- A bcrypt hash of the password
- The password encrypted with a reversible cipher
- Nothing, passwords should not be stored at all
Answer: B. A bcrypt hash of the password
Explanation: Only the bcrypt hash of the password should ever be stored, ensuring the plain-text password itself is never persisted, protecting users even if the database is compromised.
Concept link: Complete auth flow: validated DTOs → bcrypt hashing at signup → bcrypt.compare() at login → JWT issuance with sub/email/role claims.
Why this matters: A production-ready auth module is really just the disciplined combination of every individual piece covered throughout this module.
2. Why must the signup and login routes be marked @Public() in an application using a global authentication guard?
- Because they don't need to be secure
- Because requiring authentication to reach the routes that create an account or issue a token would be circular and lock everyone out
- Because @Public() makes them faster
- Because DTOs cannot be validated on protected routes
Answer: B. Because requiring authentication to reach the routes that create an account or issue a token would be circular and lock everyone out
Explanation: Signup and login must remain accessible without a prior token, since they are precisely the routes responsible for creating an account and issuing the token in the first place.
Concept link: Signup and login routes must be @Public(); everything else protected by a global JwtAuthGuard.
Why this matters: Getting the @Public() exemption right on signup/login is a small detail with an outsized impact if missed.
3. What combination of guards and decorators would restrict a route to only authenticated users with the 'admin' role?
- @Public() alone
- @UseGuards(JwtAuthGuard, RolesGuard) combined with @Roles('admin')
- @Throttle() alone
- @SkipThrottle() combined with @Public()
Answer: B. @UseGuards(JwtAuthGuard, RolesGuard) combined with @Roles('admin')
Explanation: This combination first authenticates the request via JwtAuthGuard, then checks the user's role against the route's @Roles('admin') requirement via RolesGuard, correctly restricting access to admins only.
Concept link: Role assigned at signup flows into the JWT payload, consumed later by RolesGuard for RBAC enforcement.
Why this matters: Embedding role information in the JWT payload at login time is what makes RBAC enforcement downstream effortless.
4. What claims are typically included in the JWT payload signed during login in this integrated example?
- Only the password hash
- The user's ID (sub), email, and role
- The entire raw request object
- Nothing, JWTs are always empty until refreshed
Answer: B. The user's ID (sub), email, and role
Explanation: The JWT payload signed at login includes the user's ID (conventionally under the 'sub' claim), email, and role, providing exactly the information needed for both identifying the user and enforcing role-based access control downstream.
Concept link: Adding new protected, role-restricted routes to an existing auth module is largely declarative once the foundation is built.
Why this matters: Once this foundation exists, extending an application with new protected or role-restricted features requires very little additional code.
Common Secure Login Signup API NestJS Mistakes to Avoid
- Forgetting to exempt signup and login from a global authentication guard, accidentally making it impossible for any user to ever log in or register.
- Including the password hash (or worse, a plain-text password) inside the JWT payload, unnecessarily exposing sensitive data to anyone who decodes the token.
- Assigning every new signup an 'admin' role by default instead of a safe, restrictive default like 'user', accidentally granting excessive privileges to every new account.
- Building the admin-only exercise route without both JwtAuthGuard and RolesGuard together, either skipping authentication entirely or checking roles against an unauthenticated request.
Secure Login Signup API NestJS: Interview Notes and Exam Tips
- Complete auth flow: validated DTOs → bcrypt hashing at signup → bcrypt.compare() at login → JWT issuance with sub/email/role claims.
- Signup and login routes must be @Public(); everything else protected by a global JwtAuthGuard.
- Role assigned at signup flows into the JWT payload, consumed later by RolesGuard for RBAC enforcement.
- Adding new protected, role-restricted routes to an existing auth module is largely declarative once the foundation is built.
Key Secure Login Signup API NestJS Takeaways
- A production-ready auth module is really just the disciplined combination of every individual piece covered throughout this module.
- Getting the @Public() exemption right on signup/login is a small detail with an outsized impact if missed.
- Embedding role information in the JWT payload at login time is what makes RBAC enforcement downstream effortless.
- Once this foundation exists, extending an application with new protected or role-restricted features requires very little additional code.
Secure Login Signup API NestJS: Summary
This lesson integrates every concept from Module 4 into one complete, working authentication module: validated SignupDto and LoginDto classes enforce request shape, bcrypt hashes passwords at signup and verifies them at login, a signed JWT carrying the user's ID, email, and role is issued on successful login, and a global JwtAuthGuard combined with a @Public() decorator ensures signup and login remain accessible while every other route stays protected by default. The role embedded in the JWT payload flows directly into the RBAC system from earlier in this module, demonstrated through a hands-on exercise: an admin-only GET /auth/admin/users route protected by both JwtAuthGuard and RolesGuard with @Roles('admin'). This complete, integrated example is exactly the shape of authentication module you would build and extend in a real production NestJS application.