Back to blog

Giai đoạn 3: Phát triển Backend với TypeScript

typescriptnodejsbackendapiexpress
Giai đoạn 3: Phát triển Backend với TypeScript

Chào mừng đến với Giai đoạn 3

Bạn đã hoàn thành kiến thức cơ bản về TypeScript và xây dựng ứng dụng React type-safe. Giờ là lúc xây dựng backend cho các ứng dụng đó.

Trong giai đoạn này, bạn sẽ học cách tạo REST và GraphQL API production-ready với Node.js 22+, Express.js và TypeScript. Kết nối database với Prisma, triển khai authentication, xử lý lỗi, và deploy lên production—tất cả với type safety hoàn toàn.

Thời gian: 3 tuần, 1-2 giờ/ngày
Yêu cầu: Giai đoạn 1 và Giai đoạn 2

Những gì bạn sẽ học

✅ Thiết lập dự án Node.js + TypeScript với tooling phù hợp
✅ Xây dựng REST API với Express.js và type-safe routing
✅ Tích hợp PostgreSQL/MySQL với Prisma ORM
✅ Triển khai JWT authentication và authorization
✅ Xử lý validation với Zod schemas
✅ Cấu trúc dự án theo layers (Controller/Service/Repository)
✅ Xây dựng GraphQL API với Apollo Server
✅ Viết tests với Jest và Supertest
✅ Deploy lên production với Docker và PM2
✅ Monitoring API với logging và health checks

Thiết lập dự án Node.js + TypeScript

1. Khởi tạo dự án

mkdir typescript-api
cd typescript-api
npm init -y

2. Cài đặt TypeScript và dependencies

# TypeScript core
npm install --save-dev typescript @types/node tsx nodemon
 
# Development tools
npm install --save-dev @types/express @types/cors @types/dotenv
npm install --save-dev eslint prettier eslint-config-prettier
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
 
# Runtime dependencies
npm install express cors dotenv

3. Cấu hình TypeScript (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

4. Thêm scripts vào package.json

{
  "scripts": {
    "dev": "nodemon --watch src --exec tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "lint": "eslint src/**/*.ts",
    "test": "jest"
  },
  "type": "module"
}

5. Cấu trúc dự án

src/
├── index.ts              # Entry point
├── config/               # Configuration
│   └── database.ts
├── routes/               # API routes
│   ├── index.ts
│   ├── auth.routes.ts
│   └── user.routes.ts
├── controllers/          # Request handlers
│   ├── auth.controller.ts
│   └── user.controller.ts
├── services/             # Business logic
│   ├── auth.service.ts
│   └── user.service.ts
├── repositories/         # Database access
│   └── user.repository.ts
├── middleware/           # Express middleware
│   ├── auth.middleware.ts
│   ├── error.middleware.ts
│   └── validation.middleware.ts
├── types/                # Type definitions
│   └── models.ts
└── utils/                # Utilities
    ├── logger.ts
    └── jwt.ts

Xây dựng REST API với Express.js

Entry Point (src/index.ts)

import express, { Application, Request, Response } from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import routes from './routes/index.js';
import { errorHandler } from './middleware/error.middleware.js';
 
dotenv.config();
 
const app: Application = express();
const PORT = process.env.PORT || 3000;
 
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
 
// Health check
app.get('/health', (_req: Request, res: Response) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
 
// API routes
app.use('/api', routes);
 
// Error handling
app.use(errorHandler);
 
app.listen(PORT, () => {
  console.log(`🚀 Server running on http://localhost:${PORT}`);
});

Validation với Zod

Định nghĩa schemas

// src/schemas/user.schema.ts
import { z } from 'zod';
 
export const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(2, 'Tên phải có ít nhất 2 ký tự'),
    email: z.string().email('Email không hợp lệ'),
    password: z
      .string()
      .min(8, 'Mật khẩu phải có ít nhất 8 ký tự')
      .regex(/[A-Z]/, 'Mật khẩu phải chứa ít nhất 1 chữ hoa')
      .regex(/[0-9]/, 'Mật khẩu phải chứa ít nhất 1 số'),
  }),
});
 
// Infer types từ schemas
export type CreateUserInput = z.infer<typeof createUserSchema>['body'];

Validation middleware

// src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';
 
export const validateRequest =
  (schema: AnyZodObject) => async (req: Request, res: Response, next: NextFunction) => {
    try {
      await schema.parseAsync({
        body: req.body,
        query: req.query,
        params: req.params,
      });
      next();
    } catch (error) {
      if (error instanceof ZodError) {
        return res.status(400).json({
          success: false,
          error: 'Validation failed',
          details: error.errors,
        });
      }
      next(error);
    }
  };

Tích hợp Database với Prisma

1. Cài đặt Prisma

npm install --save-dev prisma
npm install @prisma/client
npx prisma init

2. Định nghĩa schema (prisma/schema.prisma)

generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  password  String
  role      Role     @default(USER)
  isActive  Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]
 
  @@map("users")
}
 
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  @@map("posts")
}
 
enum Role {
  USER
  ADMIN
}

3. Chạy migration

# Tạo migration
npx prisma migrate dev --name init
 
# Generate TypeScript types
npx prisma generate

4. Sử dụng Prisma Client

import { prisma } from '../config/database.js';
 
// Type-safe queries với autocomplete
const users = await prisma.user.findMany({
  where: { isActive: true },
  include: { posts: true },
  orderBy: { createdAt: 'desc' },
});
 
// Create với relations
const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'John Doe',
    password: 'hashedPassword',
    posts: {
      create: [{ title: 'First Post', content: 'Hello!' }],
    },
  },
  include: { posts: true },
});

Authentication với JWT

1. JWT utility functions

// src/utils/jwt.ts
import jwt from 'jsonwebtoken';
 
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
 
export interface JwtPayload {
  userId: number;
  email: string;
  role: string;
}
 
export const generateToken = (payload: JwtPayload): string => {
  return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
};
 
export const verifyToken = (token: string): JwtPayload => {
  return jwt.verify(token, JWT_SECRET) as JwtPayload;
};

2. Authentication middleware

// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken, JwtPayload } from '../utils/jwt.js';
 
declare global {
  namespace Express {
    interface Request {
      user?: JwtPayload;
    }
  }
}
 
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
  try {
    const authHeader = req.headers.authorization;
 
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({
        success: false,
        error: 'No token provided',
      });
    }
 
    const token = authHeader.substring(7);
    const decoded = verifyToken(token);
 
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({
      success: false,
      error: 'Invalid or expired token',
    });
  }
};

3. Protected routes

import { authenticate, authorize } from '../middleware/auth.middleware.js';
 
// Chỉ authenticated users
router.delete('/users/:id', authenticate, userController.deleteUser);
 
// Chỉ admins
router.get('/admin/users', authenticate, authorize('ADMIN'), adminController.getAllUsers);

Error Handling

Custom error classes

// src/utils/errors.ts
export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public isOperational = true
  ) {
    super(message);
    Object.setPrototypeOf(this, AppError.prototype);
  }
}
 
export class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(404, message);
  }
}
 
export class ValidationError extends AppError {
  constructor(message = 'Validation failed') {
    super(400, message);
  }
}

Global error handler

// src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../utils/errors.js';
 
export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      success: false,
      error: err.message,
    });
  }
 
  console.error('Unexpected error:', err);
 
  return res.status(500).json({
    success: false,
    error: process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : err.message,
  });
};

Testing với Jest và Supertest

1. Cài đặt dependencies

npm install --save-dev jest @types/jest ts-jest supertest @types/supertest

2. Integration test example

// src/__tests__/auth.test.ts
import request from 'supertest';
import app from '../index.js';
 
describe('Authentication', () => {
  describe('POST /api/auth/register', () => {
    it('nên đăng ký user mới thành công', async () => {
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          name: 'John Doe',
          email: '[email protected]',
          password: 'Password123',
        });
 
      expect(res.status).toBe(201);
      expect(res.body.success).toBe(true);
      expect(res.body.data.user).toHaveProperty('id');
      expect(res.body.data).toHaveProperty('token');
    });
 
    it('nên trả về 400 nếu email không hợp lệ', async () => {
      const res = await request(app)
        .post('/api/auth/register')
        .send({
          name: 'Jane Doe',
          email: 'invalid-email',
          password: 'Password123',
        });
 
      expect(res.status).toBe(400);
      expect(res.body.success).toBe(false);
    });
  });
});

3. Chạy tests

# Run all tests
npm test
 
# Run với coverage
npm test -- --coverage

GraphQL API với Apollo Server

1. Định nghĩa schema

// src/graphql/schema.ts
export const typeDefs = `#graphql
  type User {
    id: ID!
    email: String!
    name: String!
    posts: [Post!]!
  }
 
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }
 
  type AuthPayload {
    user: User!
    token: String!
  }
 
  type Query {
    me: User
    users: [User!]!
    posts: [Post!]!
  }
 
  type Mutation {
    register(email: String!, password: String!, name: String!): AuthPayload!
    login(email: String!, password: String!): AuthPayload!
    createPost(title: String!, content: String!): Post!
  }
`;

2. Implement resolvers

// src/graphql/resolvers.ts
import { prisma } from '../config/database.js';
import { AuthService } from '../services/auth.service.js';
 
export const resolvers = {
  Query: {
    me: async (_parent: any, _args: any, context: Context) => {
      if (!context.user) {
        throw new GraphQLError('Not authenticated');
      }
      return prisma.user.findUnique({
        where: { id: context.user.userId },
      });
    },
 
    users: async () => {
      return prisma.user.findMany();
    },
  },
 
  Mutation: {
    register: async (_parent: any, args: any) => {
      const authService = new AuthService();
      return authService.register(args.email, args.password, args.name);
    },
 
    createPost: async (_parent: any, args: any, context: Context) => {
      if (!context.user) {
        throw new GraphQLError('Not authenticated');
      }
 
      return prisma.post.create({
        data: {
          title: args.title,
          content: args.content,
          authorId: context.user.userId,
        },
      });
    },
  },
};

Deployment

1. Production build

npm run build

2. Dockerfile

FROM node:22-alpine AS builder
 
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-alpine
 
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
COPY prisma ./prisma
 
EXPOSE 3000
CMD ["node", "dist/index.js"]

3. Docker Compose

version: '3.8'
 
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@postgres:5432/db
    depends_on:
      - postgres
 
  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=db
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
volumes:
  postgres_data:

Kế hoạch học tập 3 tuần

Tuần 1: Setup & REST API

  • Thiết lập dự án TypeScript
  • Xây dựng REST API với Express
  • Tích hợp Prisma ORM
  • Validation với Zod

Tuần 2: Authentication & GraphQL

  • JWT authentication
  • Role-based authorization
  • GraphQL API với Apollo Server
  • Advanced validation

Tuần 3: Testing & Deployment

  • Unit tests với Jest
  • Integration tests với Supertest
  • Docker containerization
  • Deploy lên production

Best Practices

✅ Sử dụng layered architecture (Controller → Service → Repository)
✅ Enable strict mode trong tsconfig.json
✅ Tránh any type - dùng unknown cho truly unknown types
✅ Validate inputs với Zod
✅ Hash passwords với bcrypt
✅ Centralize error handling trong middleware
✅ Use environment variables cho secrets
✅ Paginate large results
✅ Index database cho frequently queried columns

Common Pitfalls (Lỗi thường gặp)

❌ Không xử lý errors trong async controllers
❌ Trả về passwords cho client
❌ Hardcode secrets và configuration
❌ Bỏ qua TypeScript errors với @ts-ignore
❌ Không validate user input
❌ Thiếu connection pooling cho database

Bước tiếp theo

Sau Giai đoạn 3

Giai đoạn 4: Full-Stack Integration (sắp ra mắt)

  • Kết nối frontend và backend với tRPC
  • End-to-end type safety
  • Real-time features với WebSockets
  • Deployment strategies

Advanced Topics:

  • Microservices với TypeScript
  • Event-driven architecture
  • Message queues (Redis, RabbitMQ)
  • Background jobs (BullMQ)

Tổng kết

Trong Giai đoạn 3, bạn đã học cách xây dựng backend production-ready với TypeScript:

✅ Project setup: Cấu hình TypeScript, ESLint và tooling hiện đại
✅ REST APIs: Xây dựng Express.js API type-safe với layered architecture
✅ Database: Tích hợp Prisma ORM với PostgreSQL
✅ Validation: Sử dụng Zod cho runtime type checking
✅ Authentication: Triển khai JWT-based auth với bcrypt
✅ GraphQL: Tạo Apollo Server với resolvers
✅ Testing: Viết unit và integration tests với Jest
✅ Deployment: Containerize với Docker và deploy lên production

Hành trình full-stack:

Bắt đầu xây dựng: Cách tốt nhất để học là thực hành. Chọn một ý tưởng dự án (blog, todo app, e-commerce API) và triển khai sử dụng các patterns bạn đã học!

Sẵn sàng kết nối frontend và backend? Hãy xem Giai đoạn 4: Full-Stack Integration để có end-to-end type safety với tRPC.

Deep Dive

Muốn đi sâu hơn vào các chủ đề trong giai đoạn này?

📘 Deep Dive: Kiểu TypeScript nâng cao → — Conditional type, mapped type, branded type và nhiều hơn 📘 Deep Dive: Phát triển API Node.js → — Pattern API production-ready 📘 Deep Dive: Database & ORM → — Prisma, Drizzle và best practices


Trước đó: Giai đoạn 2: Frontend Development Tiếp theo: Giai đoạn 4: Full-Stack Integration

Happy coding! 🚀

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.