🔑 Intro
Kalau bikin backend, fitur paling wajib tuh login. Nah, di NestJS kita bisa bikin sistem login modern pake JWT (JSON Web Token). Biar lebih lengkap, biasanya ada tiga fitur utama:
- Access Token → buat akses API sehari-hari.
- Refresh Token → biar user nggak login ulang tiap kali token expired.
- Logout → biar token lama nggak bisa dipake lagi.
Di project ini kita bakal implementasi JWT Auth dengan TypeORM di NestJS. Jadi token & user bakal nyimpen di database (nggak cuma di memory), bikin sistem auth lebih rapi dan aman. Cocok banget buat yang lagi belajar bikin backend skala real project. ⚡
1️⃣ Install Dependencies
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt typeorm sqlite3
👉 aku pakai SQLite biar simple, tapi lo bisa ganti ke MySQL/Postgres.
2️⃣ User Entity
// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column()
password: string;
@Column({ default: 'user' })
role: string;
@Column({ nullable: true })
refreshToken: string;
}
3️⃣ Users Service
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private usersRepo: Repository<User>,
) {}
async findOne(username: string) {
return this.usersRepo.findOne({ where: { username } });
}
async findById(id: number) {
return this.usersRepo.findOne({ where: { id } });
}
async create(username: string, password: string, role = 'user') {
const user = this.usersRepo.create({ username, password, role });
return this.usersRepo.save(user);
}
async saveRefreshToken(userId: number, refreshToken: string) {
const hashedToken = await bcrypt.hash(refreshToken, 10);
await this.usersRepo.update(userId, { refreshToken: hashedToken });
}
async removeRefreshToken(userId: number) {
await this.usersRepo.update(userId, { refreshToken: null });
}
async validateRefreshToken(userId: number, refreshToken: string) {
const user = await this.findById(userId);
if (!user || !user.refreshToken) return false;
return bcrypt.compare(refreshToken, user.refreshToken);
}
}
4️⃣ Auth Service
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(username: string, pass: string) {
const user = await this.usersService.findOne(username);
if (user && user.password === pass) {
const { password, refreshToken, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.id, role: user.role };
const accessToken = this.jwtService.sign(payload, {
secret: process.env.JWT_SECRET || 'superSecretKey',
expiresIn: '15m',
});
const refreshToken = this.jwtService.sign(payload, {
secret: process.env.JWT_REFRESH_SECRET || 'refreshSecretKey',
expiresIn: '7d',
});
await this.usersService.saveRefreshToken(user.id, refreshToken);
return {
access_token: accessToken,
refresh_token: refreshToken,
};
}
async refreshToken(userId: number, refreshToken: string) {
const isValid = await this.usersService.validateRefreshToken(userId, refreshToken);
if (!isValid) throw new UnauthorizedException('Invalid refresh token');
const user = await this.usersService.findById(userId);
if (!user) throw new UnauthorizedException('User not found');
const payload = { username: user.username, sub: user.id, role: user.role };
return {
access_token: this.jwtService.sign(payload, {
secret: process.env.JWT_SECRET || 'superSecretKey',
expiresIn: '15m',
}),
};
}
async logout(userId: number) {
await this.usersService.removeRefreshToken(userId);
return { message: 'Logged out successfully' };
}
}
5️⃣ Auth Controller
// src/auth/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('login')
async login(@Body() body: { username: string; password: string }) {
const user = await this.authService.validateUser(body.username, body.password);
return this.authService.login(user);
}
@Post('refresh')
async refresh(@Body() body: { userId: number; refreshToken: string }) {
return this.authService.refreshToken(body.userId, body.refreshToken);
}
@Post('logout')
async logout(@Body() body: { userId: number }) {
return this.authService.logout(body.userId);
}
}
6️⃣ JWT Strategy
// src/auth/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, 'jwt') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET || 'superSecretKey',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username, role: payload.role };
}
}
7️⃣ App Module
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from './auth/auth.module';
import { UsersService } from './users/users.service';
import { User } from './users/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'db.sqlite',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forFeature([User]),
AuthModule,
],
providers: [UsersService],
})
export class AppModule {}
8️⃣ Testing Flow
1. Seed User (opsional)
Bisa bikin user dummy pakai UsersService.create().
Misalnya: username: alice, password: 1234.
2. Login
POST /auth/login
{ "username": "alice", "password": "1234" }
→ dapet access_token (15m) + refresh_token (7d)
3. Pakai Access Token
GET /profile
Authorization: Bearer <access_token>
4. Refresh Token
POST /auth/refresh
{ "userId": 1, "refreshToken": "<refresh_token>" }
→ dapet access_token baru
5. Logout
POST /auth/logout
{ "userId": 1 }
→ refresh token dihapus dari DB
🎯 Kesimpulan
- Access Token → expired cepat (aman buat request API).
- Refresh Token → disimpan di DB (hashed), expired lebih lama.
- Logout → refresh token dihapus dari DB → user bener-bener keluar.
- TypeORM bikin data user + refresh token lebih persistent, bukan sekadar array dummy.

