Back to blog

Giai đoạn 4: Tích hợp Full-Stack với TypeScript

typescriptfullstackmonorepodeploymentproduction
Giai đoạn 4: Tích hợp Full-Stack với TypeScript

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

Bạn đã thành thạo cơ bản TypeScript, xây dựng ứng dụng React có type safety, và tạo API production-ready. Giờ là lúc kết hợp tất cả lại với nhau.

Trong giai đoạn cuối cùng này, bạn sẽ học cách tích hợp frontend và backend thành một ứng dụng full-stack gắn kết với shared types, kiến trúc monorepo, type safety từ đầu đến cuối, và chiến lược deployment production.

Đây là nơi TypeScript thực sự tỏa sáng—types chảy liền mạch từ database schema qua API đến các component frontend, loại bỏ hoàn toàn các lỗi tích hợp.

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

Bạn sẽ học được gì

Kết thúc Giai đoạn 4, bạn sẽ có thể:

✅ Cấu trúc dự án full-stack với kiến trúc monorepo
✅ Chia sẻ TypeScript types giữa frontend và backend
✅ Tích hợp React với Express.js API sử dụng TanStack Query
✅ Triển khai xác thực end-to-end (JWT + React Context)
✅ Xây dựng tính năng real-time với WebSockets
✅ Xử lý upload file từ client đến server
✅ Thiết lập CI/CD pipeline với GitHub Actions
✅ Deploy ứng dụng full-stack lên production (Docker, Railway, Vercel)
✅ Giám sát và debug ứng dụng full-stack
✅ Triển khai API versioning và backwards compatibility

Kiến trúc Monorepo với NPM Workspaces

Tại sao Monorepo?

Monorepo cho phép bạn quản lý nhiều package liên quan trong một repository:

Lợi ích:

  • Shared code: Tái sử dụng types, utilities, và constants
  • Atomic changes: Cập nhật frontend và backend cùng lúc
  • Simplified tooling: Một cấu hình cho linting, testing, deployment
  • Type safety: Import shared types mà không cần publish lên npm

Nhược điểm:

  • Repository lớn hơn
  • Build phức tạp hơn
  • Yêu cầu kỷ luật trong quản lý dependencies

Thiết lập Monorepo

mkdir fullstack-app
cd fullstack-app
npm init -y

Tạo package.json với workspaces:

{
  "name": "fullstack-app",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev": "npm run dev --workspaces --if-present",
    "build": "npm run build --workspaces --if-present",
    "test": "npm run test --workspaces --if-present"
  },
  "devDependencies": {
    "typescript": "^5.7.3",
    "prettier": "^3.4.2",
    "eslint": "^9.18.0"
  }
}

Tạo cấu trúc Workspace

mkdir -p packages/shared packages/backend packages/frontend
fullstack-app/
├── package.json              # Root workspace config
├── packages/
│   ├── shared/               # Shared types & utilities
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── src/
│   │       ├── types/        # TypeScript types
│   │       ├── utils/        # Utilities chung
│   │       └── constants/    # Shared constants
│   ├── backend/              # Express.js API
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── src/
│   │       ├── routes/
│   │       ├── controllers/
│   │       └── services/
│   └── frontend/             # React app
│       ├── package.json
│       ├── tsconfig.json
│       └── src/
│           ├── components/
│           ├── pages/
│           └── hooks/

Xây dựng Shared Package

1. Khởi tạo Shared Package

packages/shared/package.json:

{
  "name": "@fullstack-app/shared",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "devDependencies": {
    "typescript": "^5.7.3"
  }
}

packages/shared/tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "CommonJS",
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

2. Định nghĩa Shared Types

packages/shared/src/types/user.ts:

export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user';
  createdAt: Date;
  updatedAt: Date;
}
 
export interface CreateUserInput {
  email: string;
  password: string;
  name: string;
}
 
export interface LoginInput {
  email: string;
  password: string;
}
 
export interface AuthResponse {
  user: User;
  token: string;
}
 
export type UserWithoutPassword = Omit<User, 'password'>;

packages/shared/src/types/post.ts:

export interface Post {
  id: string;
  title: string;
  content: string;
  slug: string;
  published: boolean;
  authorId: string;
  createdAt: Date;
  updatedAt: Date;
}
 
export interface CreatePostInput {
  title: string;
  content: string;
  slug: string;
  published?: boolean;
}
 
export interface UpdatePostInput {
  title?: string;
  content?: string;
  published?: boolean;
}
 
export interface PostWithAuthor extends Post {
  author: {
    id: string;
    name: string;
    email: string;
  };
}

packages/shared/src/types/api.ts:

// Generic API response wrapper
export interface ApiResponse<T> {
  success: boolean;
  data?: T;
  error?: string;
  message?: string;
}
 
// Pagination
export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
}
 
export interface PaginationParams {
  page?: number;
  pageSize?: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}
 
// Validation error
export interface ValidationError {
  field: string;
  message: string;
}

packages/shared/src/utils/validation.ts:

export const isValidEmail = (email: string): boolean => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};
 
export const isStrongPassword = (password: string): boolean => {
  // Ít nhất 8 ký tự, 1 chữ hoa, 1 chữ thường, 1 số
  return (
    password.length >= 8 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password)
  );
};
 
export const slugify = (text: string): string => {
  return text
    .toLowerCase()
    .trim()
    .replace(/[^\w\s-]/g, '')
    .replace(/[\s_-]+/g, '-')
    .replace(/^-+|-+$/g, '');
};

packages/shared/src/constants/index.ts:

export const API_ROUTES = {
  AUTH: {
    LOGIN: '/api/auth/login',
    REGISTER: '/api/auth/register',
    LOGOUT: '/api/auth/logout',
    REFRESH: '/api/auth/refresh',
    ME: '/api/auth/me',
  },
  POSTS: {
    LIST: '/api/posts',
    CREATE: '/api/posts',
    GET: (id: string) => `/api/posts/${id}`,
    UPDATE: (id: string) => `/api/posts/${id}`,
    DELETE: (id: string) => `/api/posts/${id}`,
  },
  USERS: {
    LIST: '/api/users',
    GET: (id: string) => `/api/users/${id}`,
    UPDATE: (id: string) => `/api/users/${id}`,
    DELETE: (id: string) => `/api/users/${id}`,
  },
} as const;
 
export const HTTP_STATUS = {
  OK: 200,
  CREATED: 201,
  NO_CONTENT: 204,
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  CONFLICT: 409,
  UNPROCESSABLE_ENTITY: 422,
  INTERNAL_SERVER_ERROR: 500,
} as const;
 
export const ERROR_MESSAGES = {
  UNAUTHORIZED: 'Bạn phải đăng nhập',
  FORBIDDEN: 'Bạn không có quyền truy cập',
  NOT_FOUND: 'Không tìm thấy tài nguyên',
  VALIDATION_ERROR: 'Validation thất bại',
  SERVER_ERROR: 'Lỗi server nội bộ',
} as const;

packages/shared/src/index.ts:

// Export tất cả types
export * from './types/user';
export * from './types/post';
export * from './types/api';
 
// Export utilities
export * from './utils/validation';
 
// Export constants
export * from './constants';

3. Build Shared Package

cd packages/shared
npm run build

Tạo ra dist/ với JavaScript đã compile và TypeScript declarations.

Tích hợp Backend với Shared Types

1. Cài đặt Shared Package trong Backend

packages/backend/package.json:

{
  "name": "@fullstack-app/backend",
  "version": "1.0.0",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@fullstack-app/shared": "*",
    "express": "^5.1.0",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "jsonwebtoken": "^9.0.2",
    "bcryptjs": "^2.4.3",
    "@prisma/client": "^6.5.0",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/cors": "^2.8.17",
    "@types/jsonwebtoken": "^9.0.7",
    "@types/bcryptjs": "^2.4.6",
    "tsx": "^4.20.0",
    "typescript": "^5.7.3",
    "prisma": "^6.5.0"
  }
}

2. Sử dụng Shared Types trong Controllers

packages/backend/src/controllers/authController.ts:

import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { prisma } from '../lib/prisma';
import {
  LoginInput,
  CreateUserInput,
  AuthResponse,
  ApiResponse,
  HTTP_STATUS,
  ERROR_MESSAGES,
} from '@fullstack-app/shared';
 
export const register = async (
  req: Request<{}, {}, CreateUserInput>,
  res: Response<ApiResponse<AuthResponse>>
) => {
  try {
    const { email, password, name } = req.body;
 
    // Kiểm tra user đã tồn tại
    const existingUser = await prisma.user.findUnique({ where: { email } });
    if (existingUser) {
      return res.status(HTTP_STATUS.CONFLICT).json({
        success: false,
        error: 'User đã tồn tại',
      });
    }
 
    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10);
 
    // Tạo user
    const user = await prisma.user.create({
      data: {
        email,
        password: hashedPassword,
        name,
      },
    });
 
    // Generate token
    const token = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET!,
      { expiresIn: '7d' }
    );
 
    res.status(HTTP_STATUS.CREATED).json({
      success: true,
      data: {
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          createdAt: user.createdAt,
          updatedAt: user.updatedAt,
        },
        token,
      },
    });
  } catch (error) {
    res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
      success: false,
      error: ERROR_MESSAGES.SERVER_ERROR,
    });
  }
};
 
export const login = async (
  req: Request<{}, {}, LoginInput>,
  res: Response<ApiResponse<AuthResponse>>
) => {
  try {
    const { email, password } = req.body;
 
    // Tìm user
    const user = await prisma.user.findUnique({ where: { email } });
    if (!user) {
      return res.status(HTTP_STATUS.UNAUTHORIZED).json({
        success: false,
        error: 'Thông tin đăng nhập không hợp lệ',
      });
    }
 
    // Kiểm tra password
    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      return res.status(HTTP_STATUS.UNAUTHORIZED).json({
        success: false,
        error: 'Thông tin đăng nhập không hợp lệ',
      });
    }
 
    // Generate token
    const token = jwt.sign(
      { userId: user.id, role: user.role },
      process.env.JWT_SECRET!,
      { expiresIn: '7d' }
    );
 
    res.json({
      success: true,
      data: {
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          createdAt: user.createdAt,
          updatedAt: user.updatedAt,
        },
        token,
      },
    });
  } catch (error) {
    res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
      success: false,
      error: ERROR_MESSAGES.SERVER_ERROR,
    });
  }
};

Tích hợp Frontend với Shared Types

1. Cài đặt Shared Package trong Frontend

packages/frontend/package.json:

{
  "name": "@fullstack-app/frontend",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@fullstack-app/shared": "*",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router-dom": "^7.1.3",
    "@tanstack/react-query": "^6.0.0",
    "axios": "^1.7.9",
    "zustand": "^5.0.3"
  },
  "devDependencies": {
    "@types/react": "^19.0.6",
    "@types/react-dom": "^19.0.2",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.7.3",
    "vite": "^6.0.7"
  }
}

2. Tạo Type-Safe API Client

packages/frontend/src/lib/api.ts:

import axios, { AxiosInstance } from 'axios';
import {
  ApiResponse,
  AuthResponse,
  LoginInput,
  CreateUserInput,
  User,
  Post,
  PostWithAuthor,
  CreatePostInput,
  UpdatePostInput,
  PaginatedResponse,
  PaginationParams,
  API_ROUTES,
} from '@fullstack-app/shared';
 
class ApiClient {
  private client: AxiosInstance;
 
  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      headers: {
        'Content-Type': 'application/json',
      },
    });
 
    // Thêm auth token vào requests
    this.client.interceptors.request.use((config) => {
      const token = localStorage.getItem('token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
  }
 
  // Auth endpoints
  async login(data: LoginInput): Promise<AuthResponse> {
    const response = await this.client.post<ApiResponse<AuthResponse>>(
      API_ROUTES.AUTH.LOGIN,
      data
    );
    return response.data.data!;
  }
 
  async register(data: CreateUserInput): Promise<AuthResponse> {
    const response = await this.client.post<ApiResponse<AuthResponse>>(
      API_ROUTES.AUTH.REGISTER,
      data
    );
    return response.data.data!;
  }
 
  async getMe(): Promise<User> {
    const response = await this.client.get<ApiResponse<User>>(
      API_ROUTES.AUTH.ME
    );
    return response.data.data!;
  }
 
  async logout(): Promise<void> {
    await this.client.post(API_ROUTES.AUTH.LOGOUT);
  }
 
  // Post endpoints
  async getPosts(params?: PaginationParams): Promise<PaginatedResponse<PostWithAuthor>> {
    const response = await this.client.get<ApiResponse<PaginatedResponse<PostWithAuthor>>>(
      API_ROUTES.POSTS.LIST,
      { params }
    );
    return response.data.data!;
  }
 
  async getPost(id: string): Promise<PostWithAuthor> {
    const response = await this.client.get<ApiResponse<PostWithAuthor>>(
      API_ROUTES.POSTS.GET(id)
    );
    return response.data.data!;
  }
 
  async createPost(data: CreatePostInput): Promise<Post> {
    const response = await this.client.post<ApiResponse<Post>>(
      API_ROUTES.POSTS.CREATE,
      data
    );
    return response.data.data!;
  }
 
  async updatePost(id: string, data: UpdatePostInput): Promise<Post> {
    const response = await this.client.put<ApiResponse<Post>>(
      API_ROUTES.POSTS.UPDATE(id),
      data
    );
    return response.data.data!;
  }
 
  async deletePost(id: string): Promise<void> {
    await this.client.delete(API_ROUTES.POSTS.DELETE(id));
  }
}
 
export const api = new ApiClient(import.meta.env.VITE_API_URL || 'http://localhost:4000');

3. Tạo Auth Store với Zustand

packages/frontend/src/stores/authStore.ts:

import { create } from 'zustand';
import { User } from '@fullstack-app/shared';
import { api } from '../lib/api';
 
interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
  logout: () => void;
  fetchUser: () => Promise<void>;
}
 
export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  token: localStorage.getItem('token'),
  isAuthenticated: !!localStorage.getItem('token'),
  isLoading: false,
 
  login: async (email, password) => {
    set({ isLoading: true });
    try {
      const { user, token } = await api.login({ email, password });
      localStorage.setItem('token', token);
      set({ user, token, isAuthenticated: true, isLoading: false });
    } catch (error) {
      set({ isLoading: false });
      throw error;
    }
  },
 
  register: async (email, password, name) => {
    set({ isLoading: true });
    try {
      const { user, token } = await api.register({ email, password, name });
      localStorage.setItem('token', token);
      set({ user, token, isAuthenticated: true, isLoading: false });
    } catch (error) {
      set({ isLoading: false });
      throw error;
    }
  },
 
  logout: () => {
    localStorage.removeItem('token');
    set({ user: null, token: null, isAuthenticated: false });
    api.logout().catch(() => {});
  },
 
  fetchUser: async () => {
    try {
      const user = await api.getMe();
      set({ user });
    } catch (error) {
      // Token invalid, xóa auth
      localStorage.removeItem('token');
      set({ user: null, token: null, isAuthenticated: false });
    }
  },
}));

Triển khai End-to-End Authentication

Backend: JWT Middleware

packages/backend/src/middleware/auth.ts:

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { HTTP_STATUS, ERROR_MESSAGES } from '@fullstack-app/shared';
 
export interface AuthRequest<P = any, ResBody = any, ReqBody = any> 
  extends Request<P, ResBody, ReqBody> {
  user?: {
    userId: string;
    role: 'admin' | 'user';
  };
}
 
export const authenticate = (
  req: AuthRequest,
  res: Response,
  next: NextFunction
) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');
    
    if (!token) {
      return res.status(HTTP_STATUS.UNAUTHORIZED).json({
        success: false,
        error: ERROR_MESSAGES.UNAUTHORIZED,
      });
    }
 
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
      userId: string;
      role: 'admin' | 'user';
    };
 
    req.user = decoded;
    next();
  } catch (error) {
    res.status(HTTP_STATUS.UNAUTHORIZED).json({
      success: false,
      error: 'Token không hợp lệ',
    });
  }
};
 
export const requireAdmin = (
  req: AuthRequest,
  res: Response,
  next: NextFunction
) => {
  if (req.user?.role !== 'admin') {
    return res.status(HTTP_STATUS.FORBIDDEN).json({
      success: false,
      error: ERROR_MESSAGES.FORBIDDEN,
    });
  }
  next();
};

Frontend: Protected Routes

packages/frontend/src/components/ProtectedRoute.tsx:

import { Navigate } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
 
interface ProtectedRouteProps {
  children: React.ReactNode;
}
 
export function ProtectedRoute({ children }: ProtectedRouteProps) {
  const { isAuthenticated } = useAuthStore();
 
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
 
  return <>{children}</>;
}

Deployment với Docker

Root Dockerfile (multi-stage):

# Stage 1: Build shared package
FROM node:22-alpine AS shared-builder
WORKDIR /app
COPY package*.json ./
COPY packages/shared/package*.json ./packages/shared/
RUN npm install --workspace=@fullstack-app/shared
COPY packages/shared ./packages/shared
RUN npm run build --workspace=@fullstack-app/shared
 
# Stage 2: Build backend
FROM node:22-alpine AS backend-builder
WORKDIR /app
COPY package*.json ./
COPY packages/backend/package*.json ./packages/backend/
COPY --from=shared-builder /app/packages/shared ./packages/shared
RUN npm install --workspace=@fullstack-app/backend
COPY packages/backend ./packages/backend
RUN npm run build --workspace=@fullstack-app/backend
 
# Stage 3: Build frontend
FROM node:22-alpine AS frontend-builder
WORKDIR /app
COPY package*.json ./
COPY packages/frontend/package*.json ./packages/frontend/
COPY --from=shared-builder /app/packages/shared ./packages/shared
RUN npm install --workspace=@fullstack-app/frontend
COPY packages/frontend ./packages/frontend
RUN npm run build --workspace=@fullstack-app/frontend
 
# Stage 4: Production backend
FROM node:22-alpine AS backend-production
WORKDIR /app
COPY --from=backend-builder /app/packages/backend/dist ./dist
COPY --from=backend-builder /app/packages/backend/package*.json ./
COPY --from=shared-builder /app/packages/shared/dist ./node_modules/@fullstack-app/shared/dist
RUN npm install --production
EXPOSE 4000
CMD ["node", "dist/index.js"]
 
# Stage 5: Production frontend (Nginx)
FROM nginx:alpine AS frontend-production
COPY --from=frontend-builder /app/packages/frontend/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

docker-compose.yml:

version: '3.8'
 
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: fullstack
      POSTGRES_PASSWORD: password
      POSTGRES_DB: fullstack_db
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
 
  backend:
    build:
      context: .
      target: backend-production
    environment:
      DATABASE_URL: postgresql://fullstack:password@postgres:5432/fullstack_db
      JWT_SECRET: your-secret-key
      NODE_ENV: production
    depends_on:
      - postgres
    ports:
      - "4000:4000"
 
  frontend:
    build:
      context: .
      target: frontend-production
    environment:
      VITE_API_URL: http://localhost:4000
    ports:
      - "80:80"
    depends_on:
      - backend
 
volumes:
  postgres-data:

CI/CD với GitHub Actions

.github/workflows/ci.yml:

name: CI/CD Pipeline
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  test-shared:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npm run build --workspace=@fullstack-app/shared
      - run: npm run test --workspace=@fullstack-app/shared
 
  test-backend:
    runs-on: ubuntu-latest
    needs: test-shared
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: password
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npm run build --workspace=@fullstack-app/shared
      - run: npm run build --workspace=@fullstack-app/backend
      - run: npm run test --workspace=@fullstack-app/backend
        env:
          DATABASE_URL: postgresql://postgres:password@localhost:5432/test_db
 
  test-frontend:
    runs-on: ubuntu-latest
    needs: test-shared
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npm run build --workspace=@fullstack-app/shared
      - run: npm run build --workspace=@fullstack-app/frontend
      - run: npm run test --workspace=@fullstack-app/frontend
 
  deploy:
    runs-on: ubuntu-latest
    needs: [test-backend, test-frontend]
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Railway
        run: |
          curl -X POST ${{ secrets.RAILWAY_WEBHOOK_URL }}

Best Practices

Cấu trúc dự án

✅ Sử dụng monorepo để chia sẻ types full-stack
✅ Giữ shared package không có dependencies (chỉ types)
✅ Version shared package với semantic versioning
✅ Document breaking changes trong shared types

Type Safety

✅ Sử dụng cấu hình TypeScript strict
✅ Định nghĩa API contracts trong shared package
✅ Type cho API responses và error states
✅ Sử dụng discriminated unions cho API responses

Tích hợp API

✅ Tạo API client tập trung
✅ Sử dụng React Query cho server state
✅ Triển khai optimistic updates
✅ Xử lý loading và error states một cách graceful

Authentication

✅ Lưu JWT trong httpOnly cookies (tốt nhất) hoặc localStorage
✅ Triển khai cơ chế token refresh
✅ Clear auth state khi nhận 401 responses
✅ Bảo vệ routes ở cả frontend và backend

Testing

✅ Viết integration tests cho API endpoints
✅ Test components với mocked API responses
✅ Test shared utilities độc lập
✅ Tự động hóa tests trong CI pipeline

Deployment

✅ Sử dụng environment variables cho configuration
✅ Triển khai health check endpoints
✅ Thiết lập logging và monitoring
✅ Sử dụng Docker cho deployments nhất quán
✅ Triển khai blue-green hoặc rolling deployments

Những sai lầm thường gặp

❌ Circular dependencies giữa các packages
✅ Giữ shared package chỉ chứa "dumb" types

❌ Quên rebuild shared package
✅ Sử dụng npm run dev trong watch mode hoặc pre-build scripts

❌ Coupling frontend trực tiếp với database schemas
✅ Tạo API types riêng và transform ở server-side

❌ Không xử lý API errors
✅ Sử dụng try-catch và hiển thị thông báo thân thiện với người dùng

❌ Lưu secrets trong frontend code
✅ Sử dụng environment variables, không bao giờ commit .env files

❌ Không version API endpoints
✅ Sử dụng /api/v1/ prefixes cho backwards compatibility

Bước tiếp theo

Chúc mừng! Bạn đã hoàn thành TypeScript Full-Stack Roadmap. Bây giờ bạn biết cách:

  • Xây dựng React frontends có type safety
  • Tạo Node.js backends robust
  • Chia sẻ types giữa các tầng
  • Tích hợp frontend và backend liền mạch
  • Deploy ứng dụng full-stack lên production

Tiếp tục học

Chủ đề nâng cao để khám phá:

  • Kiến trúc Microservices với TypeScript
  • GraphQL với TypeScript code generation
  • Server-Side Rendering (SSR) với Next.js
  • Tính năng real-time collaboration
  • Chiến lược deployment nâng cao (Kubernetes, AWS)
  • Tối ưu hóa performance và caching strategies

Các roadmap khác:

Tài nguyên


Deep Dive

Sẵn sàng thành thạo các lĩnh vực cụ thể? Xem các bài deep dive:

📘 Deep Dive: Kiểu TypeScript nâng cao → — Conditional type, mapped type, branded type 📘 Deep Dive: React với TypeScript → — Component pattern, hooks typing, performance 📘 Deep Dive: Phát triển API Node.js → — Pattern API production 📘 Deep Dive: Database & ORM → — Prisma, Drizzle, testing 📘 Deep Dive: Testing & DevOps → — Vitest, Playwright, CI/CD


Trước đó: Giai đoạn 3: Backend Development Series: Lộ trình TypeScript Full-Stack

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.