NextRush
Examples

Authentication

Implement JWT authentication with guards and protected routes.

Implement JWT authentication with guards.

This example demonstrates:

  • JWT token generation
  • Guard-based protection
  • Role-based access control
  • Refresh token flow

Security Notice

Never hardcode credentials or JWT secrets. This example reads JWT_SECRET from environment variables — always do the same in production. Add rate limiting to authentication endpoints to prevent brute-force attacks.


Project Setup

mkdir auth-api && cd auth-api
npm init -y
$ pnpm add nextrush @nextrush/controllers @nextrush/decorators @nextrush/di @nextrush/cors @nextrush/body-parser reflect-metadata jsonwebtoken bcryptjs
$ pnpm add -D @nextrush/dev typescript @types/node @types/jsonwebtoken @types/bcryptjs

File Structure

src/
├── index.ts
├── controllers/
│   ├── auth.controller.ts
│   └── user.controller.ts
├── services/
│   ├── auth.service.ts
│   └── user.service.ts
├── guards/
│   ├── auth.guard.ts
│   └── role.guard.ts
└── types/
    └── index.ts

Types

src/types/index.ts
export interface User {
  id: string;
  email: string;
  password: string; // hashed
  role: 'user' | 'admin';
  createdAt: string;
}

export interface JwtPayload {
  sub: string; // user id
  email: string;
  role: string;
  iat: number;
  exp: number;
}

export interface LoginDto {
  email: string;
  password: string;
}

export interface RegisterDto {
  email: string;
  password: string;
}

export interface TokenResponse {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

Auth Service

The auth service handles registration, login, token generation, and refresh workflows.

Complete AuthService implementation (click to expand)
src/services/auth.service.ts
import { Service } from '@nextrush/di';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { UnauthorizedError, BadRequestError, ConflictError } from 'nextrush';
import type { User, JwtPayload, LoginDto, RegisterDto, TokenResponse } from '../types';

const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '15m';
const REFRESH_EXPIRES_IN = '7d';

if (!JWT_SECRET || JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET env variable must be at least 32 characters');
}

@Service()
export class AuthService {
  // In-memory user store (use database in production)
  private users: Map<string, User> = new Map();
  private refreshTokens: Set<string> = new Set();

  constructor() {
    // Seed admin user
    this.createUser('admin@example.com', 'admin123', 'admin');
  }

  private async createUser(
    email: string,
    password: string,
    role: 'user' | 'admin' = 'user'
  ): Promise<User> {
    const hashedPassword = await bcrypt.hash(password, 10);
    const user: User = {
      id: Date.now().toString(),
      email,
      password: hashedPassword,
      role,
      createdAt: new Date().toISOString(),
    };
    this.users.set(user.id, user);
    return user;
  }

  async register(data: RegisterDto): Promise<TokenResponse> {
    // Validate
    if (!data.email || !data.password) {
      throw new BadRequestError('Email and password are required');
    }

    if (data.password.length < 6) {
      throw new BadRequestError('Password must be at least 6 characters');
    }

    // Check duplicate
    const existing = Array.from(this.users.values()).find((u) => u.email === data.email);
    if (existing) {
      throw new ConflictError('Email already registered');
    }

    // Create user
    const user = await this.createUser(data.email, data.password);

    // Generate tokens
    return this.generateTokens(user);
  }

  async login(data: LoginDto): Promise<TokenResponse> {
    // Validate
    if (!data.email || !data.password) {
      throw new BadRequestError('Email and password are required');
    }

    // Find user
    const user = Array.from(this.users.values()).find((u) => u.email === data.email);
    if (!user) {
      throw new UnauthorizedError('Invalid credentials');
    }

    // Verify password
    const isValid = await bcrypt.compare(data.password, user.password);
    if (!isValid) {
      throw new UnauthorizedError('Invalid credentials');
    }

    // Generate tokens
    return this.generateTokens(user);
  }

  async refresh(refreshToken: string): Promise<TokenResponse> {
    if (!refreshToken || !this.refreshTokens.has(refreshToken)) {
      throw new UnauthorizedError('Invalid refresh token');
    }

    try {
      const payload = jwt.verify(refreshToken, JWT_SECRET) as JwtPayload;
      const user = this.users.get(payload.sub);

      if (!user) {
        throw new UnauthorizedError('User not found');
      }

      // Invalidate old refresh token
      this.refreshTokens.delete(refreshToken);

      // Generate new tokens
      return this.generateTokens(user);
    } catch {
      throw new UnauthorizedError('Invalid refresh token');
    }
  }

  async logout(refreshToken: string): Promise<void> {
    this.refreshTokens.delete(refreshToken);
  }

  verifyToken(token: string): JwtPayload {
    try {
      return jwt.verify(token, JWT_SECRET) as JwtPayload;
    } catch {
      throw new UnauthorizedError('Invalid or expired token');
    }
  }

  getUserById(id: string): User | undefined {
    return this.users.get(id);
  }

  private generateTokens(user: User): TokenResponse {
    const payload = {
      sub: user.id,
      email: user.email,
      role: user.role,
    };

    const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
    const refreshToken = jwt.sign(payload, JWT_SECRET, { expiresIn: REFRESH_EXPIRES_IN });

    // Store refresh token
    this.refreshTokens.add(refreshToken);

    return {
      accessToken,
      refreshToken,
      expiresIn: 15 * 60, // 15 minutes in seconds
    };
  }
}

Guards

src/guards/auth.guard.ts
import type { GuardFn } from '@nextrush/decorators';
import { container } from '@nextrush/di';
import { AuthService } from '../services/auth.service';

export const AuthGuard: GuardFn = async (ctx) => {
  const authHeader = ctx.get('authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return false;
  }

  const token = authHeader.slice(7); // Remove 'Bearer '

  try {
    const authService = container.resolve(AuthService);
    const payload = authService.verifyToken(token);

    // Store user info in state
    ctx.state.user = {
      id: payload.sub,
      email: payload.email,
      role: payload.role,
    };

    return true;
  } catch {
    return false;
  }
};

The role guard uses a factory pattern — it returns a guard function configured for the specified roles.

src/guards/role.guard.ts
import type { GuardFn } from '@nextrush/decorators';

interface AuthUser {
  id: string;
  email: string;
  role: string;
}

export const RoleGuard =
  (allowedRoles: string[]): GuardFn =>
  async (ctx) => {
    const user = ctx.state.user as AuthUser | undefined;

    if (!user) {
      return false;
    }

    return allowedRoles.includes(user.role);
  };

// Convenience guard for admin-only routes
export const AdminGuard = RoleGuard(['admin']);

Auth Controller

src/controllers/auth.controller.ts
import { Controller, Post, Body, Ctx } from '@nextrush/decorators';
import type { Context } from 'nextrush';
import { AuthService } from '../services/auth.service';
import type { LoginDto, RegisterDto } from '../types';

@Controller('/auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('/register')
  async register(@Body() data: RegisterDto, @Ctx() ctx: Context) {
    const tokens = await this.authService.register(data);
    ctx.status = 201;
    return tokens;
  }

  @Post('/login')
  async login(@Body() data: LoginDto) {
    return this.authService.login(data);
  }

  @Post('/refresh')
  async refresh(@Body() body: { refreshToken: string }) {
    return this.authService.refresh(body.refreshToken);
  }

  @Post('/logout')
  async logout(@Body() body: { refreshToken: string }, @Ctx() ctx: Context) {
    await this.authService.logout(body.refreshToken);
    ctx.status = 204;
  }
}

Protected Controller

src/controllers/user.controller.ts
import { Controller, Get, UseGuard, Ctx } from '@nextrush/decorators';
import type { Context } from 'nextrush';
import { AuthGuard } from '../guards/auth.guard';
import { AdminGuard } from '../guards/role.guard';

@UseGuard(AuthGuard) // All routes require authentication
@Controller('/users')
export class UserController {
  // GET /users/me - Current user profile
  @Get('/me')
  getProfile(@Ctx() ctx: Context) {
    const user = ctx.state.user as { id: string; email: string; role: string };
    return {
      id: user.id,
      email: user.email,
      role: user.role,
    };
  }

  // GET /users/admin - Admin only
  @UseGuard(AdminGuard)
  @Get('/admin')
  adminDashboard() {
    return {
      message: 'Welcome, admin!',
      stats: { users: 100, orders: 500 },
    };
  }
}

Entry Point

src/index.ts
// reflect-metadata is auto-imported by the nextrush meta-package
import { createApp, createRouter, listen, errorHandler } from 'nextrush';
import { controllersPlugin } from 'nextrush/class';
import { cors } from '@nextrush/cors';
import { json } from '@nextrush/body-parser';

const app = createApp();
const router = createRouter();

// Error handler must be registered first
app.use(errorHandler());

// Middleware
app.use(cors());
app.use(json());

// Controllers
await app.plugin(
  controllersPlugin({
    router,
    root: './src',
    prefix: '/api',
  })
);

app.route('/', router);

// 404
app.use((ctx) => {
  ctx.status = 404;
  ctx.json({ error: 'Not Found' });
});

await listen(app, 3000);

Testing the API

# Register
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com","password":"password123"}'

# Login
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"admin123"}'
# Returns: {"accessToken":"...","refreshToken":"...","expiresIn":900}

# Access protected route
curl http://localhost:3000/users/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Admin route
curl http://localhost:3000/users/admin \
  -H "Authorization: Bearer ADMIN_ACCESS_TOKEN"

# Refresh token
curl -X POST http://localhost:3000/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"YOUR_REFRESH_TOKEN"}'

Security Considerations

This example uses in-memory storage for users and refresh tokens. In production, use a database and a store like Redis with TTL-based expiration.

  • Store users in a database, not memory
  • Use Redis for refresh tokens with TTL
  • Add rate limiting to auth endpoints
  • Use HTTPS in production
  • Validate email format before registration
  • Add account lockout after repeated failed attempts

Error Responses

The API returns structured error responses via errorHandler():

// 400 Bad Request
{
  "error": "BadRequestError",
  "message": "Email and password are required",
  "code": "BAD_REQUEST",
  "status": 400
}

// 401 Unauthorized
{
  "error": "UnauthorizedError",
  "message": "Invalid credentials",
  "code": "UNAUTHORIZED",
  "status": 401
}

// 403 Forbidden (guard rejection)
{
  "error": "GuardRejectionError",
  "message": "Access denied",
  "code": "GUARD_REJECTED",
  "status": 403
}

// 409 Conflict
{
  "error": "ConflictError",
  "message": "Email already registered",
  "code": "CONFLICT",
  "status": 409
}

Next Steps

On this page