Skip to main content

NestJS: TypeORM & Authentication

· 7 min read
Vũ Anh Tú
Share to be shared

🎯 Mục tiêu tuần này: Thiết lập database layer với TypeORM, hiểu sâu JWT authentication trong NestJS


1. CHỨC NĂNG CẦN HOÀN THÀNH

STTChức năngMô tảOutput
2.1TypeORM SetupKết nối PostgreSQL, config entitiesDatabase connected
2.2User EntityTạo User entity + migrationTable users trong DB
2.3Quiz Entity với RelationsQuiz + Questions entitiesForeign key relationships
2.4AuthModuleJWT + Passport LocalLogin endpoint, protected routes
2.5DTOs + Validationclass-validator integrationInput validation hoạt động

2. KIẾN THỨC CẦN NẮM VỮNG

2.1 TypeORM Concepts

📖 Entity

Khái niệm:

  • Entity = class đại diện cho 1 bảng trong database
  • Mỗi instance = 1 row trong bảng
  • Sử dụng decorators để define columns, relations

Core Component:

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity('users') // Tên bảng trong DB
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
email: string;

@Column()
passwordHash: string;

@Column({ nullable: true })
name: string;

@CreateDateColumn()
createdAt: Date;
}

Column Types phổ biến:

DecoratorMô tả
@PrimaryGeneratedColumn()Auto-increment primary key
@PrimaryGeneratedColumn('uuid')UUID primary key
@Column()Column thông thường
@Column({ type: 'json' })JSON column
@CreateDateColumn()Auto-set khi insert
@UpdateDateColumn()Auto-update khi update

📖 Relations

Khái niệm:

  • Định nghĩa quan hệ giữa các entities
  • OneToMany, ManyToOne, OneToOne, ManyToMany

Core Component:

// User có nhiều Quizzes
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@OneToMany(() => Quiz, (quiz) => quiz.host)
quizzes: Quiz[];
}

// Quiz thuộc về 1 User (host)
@Entity('quizzes')
export class Quiz {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
title: string;

@Column({ default: 30 })
timePerQuestion: number;

@ManyToOne(() => User, (user) => user.quizzes)
@JoinColumn({ name: 'host_id' })
host: User;

@Column()
hostId: string;

@OneToMany(() => Question, (question) => question.quiz)
questions: Question[];
}

// Question thuộc về 1 Quiz
@Entity('questions')
export class Question {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
content: string;

@Column('simple-json') // Lưu array: ["Option A", "Option B", ...]
options: string[];

@Column()
correctOptionIndex: number;

@ManyToOne(() => Quiz, (quiz) => quiz.questions)
@JoinColumn({ name: 'quiz_id' })
quiz: Quiz;
}

📖 Migration

Khái niệm:

  • File ghi lại sự thay đổi schema database
  • Có thể rollback (up/down)
  • BẮT BUỘC dùng cho production

Tại sao không dùng synchronize: true?

❌ synchronize: true (Development only)
- Tự động sync schema
- NGUY HIỂM: Có thể DROP column → MẤT DATA

✅ Migration (Production)
- Kiểm soát được thay đổi
- Có thể rollback
- Version control được

Commands:

# Tạo migration từ entity changes
npx typeorm migration:generate -d src/data-source.ts src/migrations/CreateUsers

# Chạy migration
npx typeorm migration:run -d src/data-source.ts

# Rollback migration gần nhất
npx typeorm migration:revert -d src/data-source.ts

2.2 Authentication Concepts

📖 JWT (JSON Web Token)

Khái niệm:

  • Token dạng header.payload.signature
  • Stateless: Server không lưu session
  • Self-contained: Chứa thông tin user

Structure:

eyJhbGciOiJIUzI1NiJ9.        ← Header (algorithm)
eyJ1c2VySWQiOiIxMjMifQ. ← Payload (data)
SflKxwRJSMeKKF2QT4fwpM... ← Signature (verify)

Flow:

1. User login (email + password)
2. Server verify → Tạo JWT
3. Client lưu JWT (localStorage/cookie)
4. Mỗi request: Authorization: Bearer <token>
5. Server verify token → Cho phép access

📖 Passport & Strategies

Khái niệm:

  • Passport = Authentication middleware
  • Strategy = Cách authenticate cụ thể (local, jwt, google, ...)

Core Component:

// 1. Local Strategy - Verify email/password
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' });
}

async validate(email: string, password: string): Promise<any> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user; // Attach to request.user
}
}

// 2. JWT Strategy - Verify token
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'your-secret-key',
});
}

async validate(payload: any) {
return { userId: payload.sub, email: payload.email };
}
}

📖 Guards

Khái niệm:

  • Kiểm tra điều kiện trước khi cho vào Controller
  • Return true = cho phép, false/exception = block

Core Component:

// Sử dụng Guards
@Controller('quizzes')
export class QuizController {
@Post()
@UseGuards(JwtAuthGuard) // Bảo vệ route này
create(@Body() dto: CreateQuizDto, @Request() req) {
// req.user chứa thông tin từ JWT
return this.quizService.create(dto, req.user.userId);
}
}

// Custom Auth Guard
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

2.3 DTO & Validation

Core Component:

import { IsEmail, IsString, MinLength, IsNumber, IsArray } from 'class-validator';

export class CreateUserDto {
@IsEmail()
email: string;

@IsString()
@MinLength(6)
password: string;

@IsString()
name: string;
}

export class CreateQuizDto {
@IsString()
@MinLength(3)
title: string;

@IsNumber()
timePerQuestion: number;

@IsArray()
questions: CreateQuestionDto[];
}

Setup ValidationPipe:

// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip properties không có trong DTO
forbidNonWhitelisted: true, // Throw error nếu có property lạ
transform: true, // Auto-transform types
}));

await app.listen(3000);
}

3. STEP BY STEP TRIỂN KHAI

Bước 1: Cài đặt Dependencies

npm install @nestjs/typeorm typeorm pg
npm install @nestjs/passport passport passport-local passport-jwt
npm install @nestjs/jwt bcrypt class-validator class-transformer
npm install -D @types/passport-local @types/passport-jwt @types/bcrypt

Bước 2: TypeORM Configuration

// src/app.module.ts
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'quiz_user',
password: 'quiz_password',
database: 'quiz_db',
entities: [User, Quiz, Question],
synchronize: true, // DEV ONLY!
}),
AuthModule,
QuizModule,
],
})
export class AppModule {}

Bước 3: Tạo Entities

// src/users/entities/user.entity.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
email: string;

@Column()
passwordHash: string;

@Column({ nullable: true })
name: string;

@CreateDateColumn()
createdAt: Date;

@OneToMany(() => Quiz, (quiz) => quiz.host)
quizzes: Quiz[];
}

Bước 4: AuthService

// src/auth/auth.service.ts
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private userRepo: Repository<User>,
private jwtService: JwtService,
) {}

async validateUser(email: string, password: string) {
const user = await this.userRepo.findOne({ where: { email } });
if (user && await bcrypt.compare(password, user.passwordHash)) {
const { passwordHash, ...result } = user;
return result;
}
return null;
}

async login(user: any) {
const payload = { email: user.email, sub: user.id };
return {
access_token: this.jwtService.sign(payload),
};
}

async register(dto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(dto.password, 10);
const user = this.userRepo.create({
email: dto.email,
passwordHash: hashedPassword,
name: dto.name,
});
return this.userRepo.save(user);
}
}

Bước 5: AuthController

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

@Post('register')
register(@Body() dto: CreateUserDto) {
return this.authService.register(dto);
}

@UseGuards(LocalAuthGuard)
@Post('login')
login(@Request() req) {
return this.authService.login(req.user);
}

@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
return req.user;
}
}

4. VERIFY CHECKLIST

📋 Câu hỏi LÝ THUYẾT

#Câu hỏiĐáp án mong đợi
1Entity trong TypeORM đại diện cho gì?Class đại diện cho 1 bảng trong database
2Tại sao không dùng synchronize: true cho production?Có thể mất data khi thay đổi entity
3JWT gồm những phần nào? Mỗi phần có vai trò gì?Header (algorithm), Payload (data), Signature (verify)
4Strategy trong Passport dùng để làm gì?Định nghĩa cách authenticate cụ thể
5Guard khác Middleware như thế nào?Guard có access ExecutionContext, chạy sau Middleware
6@OneToMany vs @ManyToOne khác nhau thế nào?One side có array, Many side có single reference

📋 Câu hỏi THỰC HÀNH

#Câu hỏiCách verify
1Database có bảng users?Kiểm tra PostgreSQL với psql hoặc pgAdmin
2Register user mới được không?POST /auth/register → 201 Created
3Login nhận được JWT?POST /auth/login → { access_token: "..." }
4Protected route hoạt động?GET /auth/profile với Bearer token
5Validation có bắt lỗi?POST với email invalid → 400 Bad Request

5. MỞ RỘNG - BEST PRACTICES

5.1 Password Security

// ❌ KHÔNG BAO GIỜ lưu plain password
user.password = password;

// ✅ Hash với bcrypt, salt rounds = 10-12
const hash = await bcrypt.hash(password, 10);

5.2 JWT Best Practices

PracticeLý do
Short expiration (15-60 min)Giảm damage nếu token bị leak
Refresh tokenKhông cần re-login thường xuyên
Secret key từ envKhông hardcode trong code
HTTPS onlyToken có thể bị intercept qua HTTP

5.3 TypeORM Performance

// ❌ N+1 problem
const quizzes = await quizRepo.find();
for (const quiz of quizzes) {
console.log(quiz.questions); // Mỗi lần là 1 query!
}

// ✅ Eager loading
const quizzes = await quizRepo.find({
relations: ['questions'], // 1 query với JOIN
});

5.4 Real-world Use Cases

PatternUse Case trong Quiz App
JWT AuthHost login để tạo/quản lý quiz
Refresh TokenUser không cần re-login mỗi 15 phút
Password ResetForgot password flow
Role-basedHOST vs PLAYER permissions

📅 Timeline Tuần 2

NgàyHoạt độngThời gian
Ngày 1-2Setup TypeORM + Entities3-4 giờ
Ngày 3-4AuthModule (JWT + Passport)4-5 giờ
Ngày 5-6DTOs, Validation, Testing2-3 giờ
Ngày 7Review + Verify questions1-2 giờ

💡 Tip: Sau tuần 2, bạn nên có thể giải thích JWT flow và tại sao lại cần Passport strategies.