Back to blog

Build a URL Shortener: Setup Project & API

typescriptnodejsexpressapibackend
Build a URL Shortener: Setup Project & API

Giờ là lúc viết code. Trong bài này, chúng ta sẽ đi từ một thư mục trống đến URL shortener API hoạt động được. Bạn paste một URL dài, nhận lại short code, và truy cập short code đó để được redirect — tất cả chạy local với in-memory storage.

Chưa có database. Chưa có Redis. Chưa có frontend. Chỉ Express, TypeScript, và clean API design. Chúng ta sẽ thêm persistence ở bài tiếp theo.

Thời gian: 1–2 giờ
Prerequisites: Tổng Quan Series & Kiến Trúc

Những gì chúng ta sẽ build trong bài này:
✅ Express + TypeScript project với folder structure chuẩn
✅ POST /api/shorten — shorten URL và nhận short code
✅ GET /:code — redirect đến URL gốc
✅ GET /api/urls/:code — lấy thông tin URL và click count
✅ URL validation, error handling middleware và environment config


Project Scaffolding

Khởi tạo project:

mkdir url-shortener && cd url-shortener
npm init -y

Install Dependencies

# Runtime dependencies
npm install express dotenv nanoid@3
 
# Development dependencies
npm install -D typescript @types/express @types/node tsx nodemon

Tại sao chọn các packages này:

  • express — HTTP framework
  • dotenv — load environment variables từ .env
  • nanoid@3 — fast, secure, URL-friendly ID generator (v3 cho CommonJS compatibility)
  • tsx — chạy TypeScript trực tiếp không cần build step (development)
  • nodemon — auto-restart khi file thay đổi

TypeScript Configuration

npx tsc --init

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Những lựa chọn quan trọng:

  • target: ES2022 — JavaScript features hiện đại (top-level await, Array.at())
  • strict: true — bắt bugs sớm với full type checking
  • outDir/rootDir — tách biệt rõ ràng giữa source và build output

NPM Scripts

Update package.json:

{
  "scripts": {
    "dev": "nodemon --watch src --ext ts --exec tsx src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "lint": "tsc --noEmit"
  }
}

Environment Configuration

Tạo .env:

PORT=3000
BASE_URL=http://localhost:3000
NODE_ENV=development

Tạo .env.example (commit vào git):

PORT=3000
BASE_URL=http://localhost:3000
NODE_ENV=development

Thêm .gitignore:

node_modules/
dist/
.env
*.log

Folder Structure

Đây là cấu trúc chúng ta sẽ build. Cố tình đơn giản lúc đầu — sẽ thêm directories khi project phát triển:

url-shortener/
├── src/
│   ├── config/
│   │   └── env.ts              # Load environment variables
│   ├── middleware/
│   │   ├── errorHandler.ts     # Global error handling
│   │   └── validateRequest.ts  # Request validation helper
│   ├── routes/
│   │   ├── urlRoutes.ts        # URL shortening endpoints
│   │   └── redirectRoutes.ts   # Redirect endpoint
│   ├── services/
│   │   └── urlService.ts       # Business logic
│   ├── store/
│   │   └── memoryStore.ts      # In-memory URL storage
│   ├── utils/
│   │   └── shortCode.ts        # Short code generation
│   ├── types/
│   │   └── url.ts              # TypeScript interfaces
│   ├── app.ts                  # Express app setup
│   └── index.ts                # Server entry point
├── .env
├── .env.example
├── .gitignore
├── package.json
└── tsconfig.json

Tại sao cấu trúc này:

  • config/ — centralized environment config với validation
  • routes/ — route definitions tách riêng khỏi logic
  • services/ — business logic decouple khỏi HTTP layer
  • store/ — data access layer (memory hiện tại, PostgreSQL sau)
  • middleware/ — reusable Express middleware
  • types/ — shared TypeScript interfaces

Đây không phải overengineering — đây là cấu trúc tối thiểu cho phép chúng ta swap storage layer mà không cần rewrite routes hay services.


Type Definitions

Bắt đầu với các core types. Chúng define URL record trông như thế nào:

// src/types/url.ts
 
export interface UrlRecord {
  id: string;
  shortCode: string;
  originalUrl: string;
  createdAt: Date;
  clickCount: number;
}
 
export interface CreateUrlRequest {
  url: string;
  customAlias?: string;
}
 
export interface CreateUrlResponse {
  shortCode: string;
  shortUrl: string;
  originalUrl: string;
  createdAt: Date;
}
 
export interface UrlStatsResponse {
  shortCode: string;
  shortUrl: string;
  originalUrl: string;
  createdAt: Date;
  clickCount: number;
}

Tách request/response types khỏi internal record cho chúng ta flexibility. UrlRecord có field id, nhưng chúng ta không expose nó trong API responses — short codes là public identifier.


Environment Configuration

Load và validate environment variables một lần lúc startup:

// src/config/env.ts
 
import dotenv from 'dotenv';
 
dotenv.config();
 
export const env = {
  port: parseInt(process.env.PORT || '3000', 10),
  baseUrl: process.env.BASE_URL || 'http://localhost:3000',
  nodeEnv: process.env.NODE_ENV || 'development',
} as const;

Dùng as const khiến các values này readonly ở type level. Nếu variable nào thiếu, chúng ta fall back về defaults hợp lý.


In-Memory Storage

Trước khi thêm database, chúng ta cần nơi lưu URLs. Map là hoàn hảo cho development:

// src/store/memoryStore.ts
 
import { UrlRecord } from '../types/url';
 
const urls = new Map<string, UrlRecord>();
 
export const memoryStore = {
  save(record: UrlRecord): UrlRecord {
    urls.set(record.shortCode, record);
    return record;
  },
 
  findByShortCode(shortCode: string): UrlRecord | undefined {
    return urls.get(shortCode);
  },
 
  findByOriginalUrl(originalUrl: string): UrlRecord | undefined {
    for (const record of urls.values()) {
      if (record.originalUrl === originalUrl) {
        return record;
      }
    }
    return undefined;
  },
 
  incrementClickCount(shortCode: string): void {
    const record = urls.get(shortCode);
    if (record) {
      record.clickCount += 1;
    }
  },
 
  exists(shortCode: string): boolean {
    return urls.has(shortCode);
  },
};

Interface ở đây — save, findByShortCode, findByOriginalUrl, incrementClickCount — giống hệt những gì chúng ta sẽ implement với Prisma sau. Khi swap sang PostgreSQL, chỉ file này thay đổi.


Short Code Generation

Trái tim của mọi URL shortener. Chúng ta cần codes mà:

  • Ngắn — 7 ký tự mặc định
  • URL-safe — chỉ ký tự alphanumeric
  • Unique — không collision (hoặc ít nhất, handle chúng)
// src/utils/shortCode.ts
 
import { nanoid, customAlphabet } from 'nanoid';
 
// Base62 alphabet: a-z, A-Z, 0-9
const ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const DEFAULT_LENGTH = 7;
 
const generate = customAlphabet(ALPHABET, DEFAULT_LENGTH);
 
export function generateShortCode(length: number = DEFAULT_LENGTH): string {
  return generate(length);
}
 
export function isValidCustomAlias(alias: string): boolean {
  if (alias.length < 3 || alias.length > 30) {
    return false;
  }
 
  // Cho phép alphanumeric, hyphens và underscores
  const validPattern = /^[a-zA-Z0-9_-]+$/;
  return validPattern.test(alias);
}

Tại Sao nanoid Thay Vì UUID Hay Math.random?

ApproachVí dụĐộ dàiURL-safeCollision Risk
UUID v4550e8400-e29b-41d4-a716-44665544000036Không (hyphens)Gần như 0
Math.random0.7381629Thay đổiKhông (dấu chấm)Cao
nanoid (Base62)aBc1X9z7CóThấp*

*Với 7 ký tự từ bảng chữ cái 62 ký tự, chúng ta có 62^7 = 3.5 nghìn tỷ codes khả dụng. Với 1,000 URLs mỗi ngày, phải mất ~9.6 triệu năm mới hết. Dù vậy chúng ta vẫn sẽ handle collisions ở Bài #4.

Tại Sao customAlphabet?

nanoid() mặc định dùng A-Za-z0-9_- (64 ký tự). Chúng ta dùng customAlphabet để giữ Base62 thuần (62 ký tự) — không hyphens hay underscores trong codes. Giữ short URLs clean và tránh vấn đề với URL parsing.


URL Validation

Chúng ta cần validate rằng users submit URLs thật, không phải rác:

// src/middleware/validateRequest.ts
 
import { Request, Response, NextFunction } from 'express';
 
export function validateShortenRequest(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const { url, customAlias } = req.body;
 
  // Kiểm tra URL có tồn tại
  if (!url || typeof url !== 'string') {
    res.status(400).json({
      error: 'URL is required',
      message: 'Please provide a valid URL in the request body',
    });
    return;
  }
 
  // Trim và validate URL format
  const trimmedUrl = url.trim();
 
  try {
    const parsed = new URL(trimmedUrl);
 
    // Chỉ cho phép http và https protocols
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      res.status(400).json({
        error: 'Invalid protocol',
        message: 'Only HTTP and HTTPS URLs are supported',
      });
      return;
    }
  } catch {
    res.status(400).json({
      error: 'Invalid URL',
      message: 'The provided string is not a valid URL',
    });
    return;
  }
 
  // Validate custom alias nếu có
  if (customAlias !== undefined) {
    if (typeof customAlias !== 'string') {
      res.status(400).json({
        error: 'Invalid alias',
        message: 'Custom alias must be a string',
      });
      return;
    }
 
    const validPattern = /^[a-zA-Z0-9_-]+$/;
    if (customAlias.length < 3 || customAlias.length > 30 || !validPattern.test(customAlias)) {
      res.status(400).json({
        error: 'Invalid alias',
        message: 'Custom alias must be 3-30 characters, alphanumeric with hyphens and underscores',
      });
      return;
    }
  }
 
  // Gắn cleaned URL vào request
  req.body.url = trimmedUrl;
  next();
}

Tại Sao URL Validation Quan Trọng

Không có validation, attacker có thể:

  • Submit javascript:alert('xss') — XSS qua redirect
  • Submit file:///etc/passwd — truy cập file local
  • Submit empty strings hoặc payloads khổng lồ — crash server

Chúng ta giới hạn protocol http: và https: và dùng built-in URL constructor, xử lý edge cases như malformed Unicode và missing TLDs.


URL Service (Business Logic)

Service layer chứa toàn bộ logic. Routes gọi service — service không biết gì về Express:

// src/services/urlService.ts
 
import { memoryStore } from '../store/memoryStore';
import { generateShortCode, isValidCustomAlias } from '../utils/shortCode';
import { env } from '../config/env';
import { CreateUrlResponse, UrlStatsResponse } from '../types/url';
 
export class UrlService {
  async shortenUrl(originalUrl: string, customAlias?: string): Promise<CreateUrlResponse> {
    // Kiểm tra URL đã được shorten chưa
    const existing = memoryStore.findByOriginalUrl(originalUrl);
    if (existing && !customAlias) {
      return this.toCreateResponse(existing.shortCode, existing.originalUrl, existing.createdAt);
    }
 
    // Dùng custom alias hoặc generate short code
    let shortCode: string;
 
    if (customAlias) {
      if (!isValidCustomAlias(customAlias)) {
        throw new AppError('Invalid custom alias. Use 3-30 alphanumeric characters, hyphens, or underscores.', 400);
      }
 
      if (memoryStore.exists(customAlias)) {
        throw new AppError('Custom alias is already taken', 409);
      }
 
      shortCode = customAlias;
    } else {
      shortCode = await this.generateUniqueCode();
    }
 
    // Lưu record
    const record = memoryStore.save({
      id: crypto.randomUUID(),
      shortCode,
      originalUrl,
      createdAt: new Date(),
      clickCount: 0,
    });
 
    return this.toCreateResponse(record.shortCode, record.originalUrl, record.createdAt);
  }
 
  async resolveAndTrack(shortCode: string): Promise<string> {
    const record = memoryStore.findByShortCode(shortCode);
 
    if (!record) {
      throw new AppError('Short URL not found', 404);
    }
 
    // Tăng click count
    memoryStore.incrementClickCount(shortCode);
 
    return record.originalUrl;
  }
 
  async getUrlStats(shortCode: string): Promise<UrlStatsResponse> {
    const record = memoryStore.findByShortCode(shortCode);
 
    if (!record) {
      throw new AppError('Short URL not found', 404);
    }
 
    return {
      shortCode: record.shortCode,
      shortUrl: `${env.baseUrl}/${record.shortCode}`,
      originalUrl: record.originalUrl,
      createdAt: record.createdAt,
      clickCount: record.clickCount,
    };
  }
 
  private async generateUniqueCode(): Promise<string> {
    const MAX_RETRIES = 5;
 
    for (let i = 0; i < MAX_RETRIES; i++) {
      const code = generateShortCode();
      if (!memoryStore.exists(code)) {
        return code;
      }
    }
 
    throw new AppError('Failed to generate unique short code. Please try again.', 500);
  }
 
  private toCreateResponse(shortCode: string, originalUrl: string, createdAt: Date): CreateUrlResponse {
    return {
      shortCode,
      shortUrl: `${env.baseUrl}/${shortCode}`,
      originalUrl,
      createdAt,
    };
  }
}
 
// Custom error class với HTTP status code
export class AppError extends Error {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message);
    this.name = 'AppError';
  }
}

Key Design Decisions

URL deduplication — nếu ai đó shorten cùng URL hai lần (không dùng custom alias), chúng ta trả về short code hiện có. Ngăn database đầy duplicates. Chúng ta sẽ làm cho configurable sau.

Retry loop cho collisions — generateUniqueCode() thử tối đa 5 lần để tìm code chưa dùng. Với 3.5 nghìn tỷ codes 7 ký tự khả dụng, collisions cực kỳ hiếm. Nhưng handle chúng là sự khác biệt giữa demo và hệ thống thực.

Service là async — dù memoryStore là synchronous, chúng ta dùng async methods. Điều này có nghĩa zero changes khi swap sang Prisma async database calls.


Error Handling Middleware

Express cần centralized error handler. Không có nó, unhandled errors sẽ crash server:

// src/middleware/errorHandler.ts
 
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../services/urlService';
 
export function errorHandler(
  err: Error,
  _req: Request,
  res: Response,
  _next: NextFunction
): void {
  // Handle known application errors
  if (err instanceof AppError) {
    res.status(err.statusCode).json({
      error: err.message,
    });
    return;
  }
 
  // Handle JSON parse errors
  if (err.type === 'entity.parse.failed') {
    res.status(400).json({
      error: 'Invalid JSON in request body',
    });
    return;
  }
 
  // Log unexpected errors
  console.error('Unexpected error:', err);
 
  res.status(500).json({
    error: 'Internal server error',
  });
}

Class AppError mang theo HTTP status code. Service throw AppError với code đúng (400, 404, 409), và error handler chuyển nó thành JSON response. Unknown errors luôn trả 500 — không bao giờ leak stack traces cho clients.

Lưu ý: Check err.type dùng property được Express body-parser đặt khi JSON parsing fail. TypeScript có thể flag — bạn có thể dùng (err as any).type hoặc extend Error type.


Routes

URL Shortening Routes

// src/routes/urlRoutes.ts
 
import { Router, Request, Response, NextFunction } from 'express';
import { UrlService } from '../services/urlService';
import { validateShortenRequest } from '../middleware/validateRequest';
 
const router = Router();
const urlService = new UrlService();
 
// POST /api/shorten — tạo short URL
router.post(
  '/shorten',
  validateShortenRequest,
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const { url, customAlias } = req.body;
      const result = await urlService.shortenUrl(url, customAlias);
      res.status(201).json(result);
    } catch (error) {
      next(error);
    }
  }
);
 
// GET /api/urls/:code — lấy thông tin URL và stats
router.get(
  '/urls/:code',
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const stats = await urlService.getUrlStats(req.params.code);
      res.json(stats);
    } catch (error) {
      next(error);
    }
  }
);
 
export default router;

Redirect Routes

// src/routes/redirectRoutes.ts
 
import { Router, Request, Response, NextFunction } from 'express';
import { UrlService } from '../services/urlService';
 
const router = Router();
const urlService = new UrlService();
 
// GET /:code — redirect đến URL gốc
router.get(
  '/:code',
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const originalUrl = await urlService.resolveAndTrack(req.params.code);
      res.redirect(302, originalUrl);
    } catch (error) {
      next(error);
    }
  }
);
 
export default router;

Tại Sao Tách Riêng Route Files?

Redirect route (GET /:code) mount ở root. API routes (/api/*) mount dưới prefix. Tách riêng giúp thứ tự mount rõ ràng trong app.ts — API routes phải đặt trước catch-all redirect route.

301 vs 302 Redirects

Chúng ta dùng 302 (temporary redirect) hiện tại. Lý do:

StatusLoạiHành vi BrowserSEO Effect
301PermanentCache mãi mãi, không bao giờ hit server nữaChuyển link equity sang destination
302TemporaryHit server mỗi lầnKhông chuyển link equity

Chúng ta muốn 302 vì:

  1. Analytics — mỗi redirect đi qua server, để đếm clicks
  2. Flexibility — có thể thay destination URL sau
  3. Expiration — có thể disable short URLs mà browsers không cache redirect cũ

Chúng ta sẽ làm configurable ở bài sau. Một số use cases (như permanent marketing links) hưởng lợi từ 301.


Express App Setup

Kết nối mọi thứ lại:

// src/app.ts
 
import express from 'express';
import urlRoutes from './routes/urlRoutes';
import redirectRoutes from './routes/redirectRoutes';
import { errorHandler } from './middleware/errorHandler';
 
const app = express();
 
// Middleware
app.use(express.json({ limit: '10kb' }));
 
// Health check
app.get('/health', (_req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
 
// API routes (phải đặt trước redirect catch-all)
app.use('/api', urlRoutes);
 
// Redirect catch-all (phải đặt cuối)
app.use(redirectRoutes);
 
// Error handler (phải đặt sau tất cả routes)
app.use(errorHandler);
 
export default app;

Thứ tự quan trọng:

  1. express.json() — parse JSON bodies trước
  2. /health — health check trước logic phức tạp
  3. /api/* — API routes match trước
  4. /:code — catch-all redirect match cuối
  5. errorHandler — bắt mọi errors từ trên

Nếu đặt /:code trước /api/*, requests tới /api/shorten sẽ match /:code với code = "api" và cố redirect — không phải điều chúng ta muốn.

Body Size Limit

express.json({ limit: '10kb' }) ngăn attackers gửi JSON payloads khổng lồ. Một URL thường dưới 2KB. 10KB cho đủ chỗ trong khi block abuse.


Server Entry Point

// src/index.ts
 
import app from './app';
import { env } from './config/env';
 
const server = app.listen(env.port, () => {
  console.log(`🚀 URL Shortener running at ${env.baseUrl}`);
  console.log(`   Environment: ${env.nodeEnv}`);
  console.log(`   Health check: ${env.baseUrl}/health`);
});
 
// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received. Shutting down gracefully...');
  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });
});

Graceful shutdown quan trọng ngay cả trong development. Khi bạn nhấn Ctrl+C, server hoàn thành xử lý các in-flight requests trước khi exit thay vì drop connections.


Test Thử

Start development server:

npm run dev

Bạn sẽ thấy:

🚀 URL Shortener running at http://localhost:3000
   Environment: development
   Health check: http://localhost:3000/health

Test 1: Health Check

curl http://localhost:3000/health
{
  "status": "ok",
  "timestamp": "2026-03-20T10:00:00.000Z"
}

Test 2: Shorten một URL

curl -X POST http://localhost:3000/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "https://github.com/expressjs/express"}'
{
  "shortCode": "aBc1X9z",
  "shortUrl": "http://localhost:3000/aBc1X9z",
  "originalUrl": "https://github.com/expressjs/express",
  "createdAt": "2026-03-20T10:00:05.000Z"
}

Test 3: Shorten với Custom Alias

curl -X POST http://localhost:3000/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "https://github.com/expressjs/express", "customAlias": "express-repo"}'
{
  "shortCode": "express-repo",
  "shortUrl": "http://localhost:3000/express-repo",
  "originalUrl": "https://github.com/expressjs/express",
  "createdAt": "2026-03-20T10:00:10.000Z"
}

Test 4: Redirect

curl -v http://localhost:3000/aBc1X9z
< HTTP/1.1 302 Found
< Location: https://github.com/expressjs/express

Hoặc mở http://localhost:3000/aBc1X9z trong browser — nó redirect sang GitHub.

Test 5: Lấy URL Stats

curl http://localhost:3000/api/urls/aBc1X9z
{
  "shortCode": "aBc1X9z",
  "shortUrl": "http://localhost:3000/aBc1X9z",
  "originalUrl": "https://github.com/expressjs/express",
  "createdAt": "2026-03-20T10:00:05.000Z",
  "clickCount": 1
}

Test 6: Error Cases

URL không hợp lệ:

curl -X POST http://localhost:3000/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "not-a-url"}'
{
  "error": "Invalid URL",
  "message": "The provided string is not a valid URL"
}

Thiếu URL:

curl -X POST http://localhost:3000/api/shorten \
  -H "Content-Type: application/json" \
  -d '{}'
{
  "error": "URL is required",
  "message": "Please provide a valid URL in the request body"
}

Short code không tồn tại:

curl http://localhost:3000/api/urls/doesnotexist
{
  "error": "Short URL not found"
}

Duplicate custom alias:

curl -X POST http://localhost:3000/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "customAlias": "express-repo"}'
{
  "error": "Custom alias is already taken"
}

Hiểu Request Flow

Hãy trace chuyện gì xảy ra khi user shorten một URL:

Và chuyện gì xảy ra khi redirect:


Trạng Thái Project Hiện Tại

Đây là file tree hoàn chỉnh sau bài này:

url-shortener/
├── src/
│   ├── config/
│   │   └── env.ts
│   ├── middleware/
│   │   ├── errorHandler.ts
│   │   └── validateRequest.ts
│   ├── routes/
│   │   ├── urlRoutes.ts
│   │   └── redirectRoutes.ts
│   ├── services/
│   │   └── urlService.ts
│   ├── store/
│   │   └── memoryStore.ts
│   ├── utils/
│   │   └── shortCode.ts
│   ├── types/
│   │   └── url.ts
│   ├── app.ts
│   └── index.ts
├── .env
├── .env.example
├── .gitignore
├── package.json
└── tsconfig.json

Tóm Tắt API

MethodEndpointMô tảStatus Codes
POST/api/shortenShorten một URL201, 400, 409, 500
GET/api/urls/:codeLấy thông tin URL và stats200, 404
GET/:codeRedirect đến URL gốc302, 404
GET/healthHealth check200

Sai Lầm Thường Gặp Cần Tránh

1. Không Validate URLs

Không bao giờ trust user input. Không có validation, ai đó có thể submit javascript:alert(1) và shortener của bạn trở thành XSS vector.

2. Thứ Tự Routes Quan Trọng

// SAI — /:code bắt hết mọi thứ, kể cả /api/*
app.use(redirectRoutes);  // GET /:code
app.use('/api', urlRoutes);
 
// ĐÚNG — /api/* match trước
app.use('/api', urlRoutes);
app.use(redirectRoutes);  // GET /:code

3. Quên Error Handling Trong Async Routes

// SAI — uncaught async error crash server
router.post('/shorten', async (req, res) => {
  const result = await urlService.shortenUrl(req.body.url);
  res.json(result);
});
 
// ĐÚNG — errors forward sang error handler
router.post('/shorten', async (req, res, next) => {
  try {
    const result = await urlService.shortenUrl(req.body.url);
    res.json(result);
  } catch (error) {
    next(error);
  }
});

4. Dùng 301 Cho Redirects Quá Sớm

301 (permanent redirect) bị browsers cache — bạn không thể undo. Bắt đầu với 302 và chỉ chuyển sang 301 khi chắc chắn destination sẽ không thay đổi.


Tiếp Theo Là Gì?

URL shortener của chúng ta hoạt động rồi, nhưng nó quên hết mọi thứ khi server restart. Trong Bài #3, chúng ta sẽ thêm PostgreSQL với Prisma ORM:

  • Docker Compose cho PostgreSQL
  • Prisma schema design với migrations
  • Thay memoryStore bằng database queries
  • Indexing strategy cho fast short code lookups
  • Connection pooling configuration

Cái đẹp của layered architecture: chỉ store/ layer thay đổi. Routes và services giữ nguyên.

Series: Build a URL Shortener
Previous: Tổng Quan Series & Kiến Trúc
Next: Phase 2: Database Design & URL Storage

📬 Đă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.