Stateless Tokens for APIs
JWT Authentication
Issue a JWT on login, verify it on every request via a JwtStrategy, and guard routes that need an authenticated user.
What you'll learn
- Configure JwtModule.registerAsync
- Implement a JwtStrategy
- Use a JwtAuthGuard on routes
JWT (JSON Web Token) auth is stateless: the server signs a token, the
client stores it, and every request carries it in an Authorization
header. No session table, no sticky sessions, no Redis required.
Install
npm i @nestjs/jwt @nestjs/passport passport passport-jwt
npm i -D @types/passport-jwt Configure JwtModule
JwtModule.registerAsync is the same async-factory pattern as the
database modules. The secret comes from config, never from code.
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
secret: cfg.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
}),
],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {} Fifteen minutes is a reasonable default for access tokens. Pair it with a longer-lived refresh token if you need persistent sessions.
Sign on Login
@Injectable()
export class AuthService {
constructor(private readonly jwt: JwtService) {}
async login(user: User) {
const payload = { sub: user.id, email: user.email };
return { accessToken: await this.jwt.signAsync(payload) };
}
} The sub (subject) claim is the standard place for the user id. Don’t
shove the entire user object into the token — keep it small, since every
request carries it.
Verify on Every Request
passport-jwt does the heavy lifting: extract from header, verify
signature, parse the payload. You only implement validate.
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(cfg: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: cfg.get<string>('JWT_SECRET'),
});
}
validate(payload: { sub: number; email: string }) {
return { id: payload.sub, email: payload.email };
}
} Whatever validate returns becomes req.user. Some teams query the
database here to confirm the user still exists — fine for security but
adds a query to every request.
Guard Your Routes
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
@Controller('me')
@UseGuards(JwtAuthGuard)
export class MeController {
@Get()
whoami(@Request() req) {
return req.user;
}
} Apply the guard globally with APP_GUARD if most routes need auth,
and create a @Public() decorator for the few that don’t.