+3

Tạo Server Node.js Đầu Tiên: Vượt Qua "Hello World" Để Chạm Tới Chuẩn Production

Bất kỳ ai học Node.js cũng bắt đầu bằng bài toán "Tạo Server". Chỉ tốn đúng 5 dòng code và 10 giây để in ra chữ "Hello World". Nhưng mang 5 dòng code đó lên môi trường Production thì chẳng khác nào xây nhà lầu trên nền cát.

Dưới đây là bài viết hướng dẫn bạn tự tay "đổ móng" cho một Server Node.js chuẩn Vibe Coder, sẵn sàng chịu tải và không bao giờ chết yểu.

Nếu bạn lên mạng gõ "How to create Node.js server", 99% các bài tutorial sẽ ném cho bạn đoạn code huyền thoại này của Express:

// Tư duy "Thợ gõ" - Nhanh, gọn, lẹ nhưng mong manh
const express = require('express');
const app = express();

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(3000, () => console.log('Server chạy ở port 3000'));

Nó chạy được không? Có. Nhưng nếu bạn bê nguyên file index.js này lên server thật, bạn sẽ sớm đối mặt với những câu hỏi chí mạng:

  • Nếu viết Unit Test cho API, làm sao để test mà không bị kẹt port 3000?
  • Nếu Docker hoặc PM2 khởi động lại server, các user đang tải dở file có bị ngắt kết nối ngang xương không?
  • Lỗi bảo mật lộ Header của Express xử lý ở đâu?

Một Vibe Coder không viết code chỉ để chạy. Chúng ta viết code để hệ thống sống sót và dễ dàng mở rộng. Hãy cùng "đập đi xây lại" cái server này nhé.

1. Nguyên Tắc Số 1: Tách Bạch App và Server

Sai lầm lớn nhất của file "Hello World" là nó gộp chung định nghĩa ứng dụng (App logic) và bộ lắng nghe mạng (Network Server) vào cùng một chỗ.

Chúng ta phải tách nó ra làm 2 file riêng biệt: app.jsserver.js.

File app.js: Nơi chứa linh hồn của ứng dụng File này KHÔNG ĐƯỢC PHÉP chứa hàm listen(). Nó chỉ làm nhiệm vụ khai báo router, middleware và logic.

// app.js
const express = require('express');
const app = express();

// Middleware giải mã JSON body
app.use(express.json());

// Khai báo các Routes
app.get('/api/v1/health', (req, res) => {
    res.status(200).json({ status: 'OK', vibe: 'Cực mượt' });
});

// Xuất app ra để nơi khác dùng
module.exports = app;

Lợi ích: Khi bạn viết Unit Test (bằng Jest/Supertest), bạn chỉ cần require('./app'). Code test sẽ giả lập gửi request thẳng vào App mà không cần mở Port mạng thực tế, test chạy nhanh như chớp và không bị lỗi "Port already in use".

2. Nguyên Tắc Số 2: Mặc Áo Giáp Bảo Vệ Ngay Từ Cửa

Trước khi mang App ra hứng traffic, bạn phải trang bị cho nó 2 tấm khiên cơ bản nhất: cors (chống lỗi Cross-Origin từ Frontend) và helmet (giấu các thông tin nhạy cảm của Express để chống Hacker dò quét).

Cài đặt: npm install cors helmet

Cập nhật lại app.js:

// app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');

const app = express();

// Bọc giáp bảo mật HTTP Headers
app.use(helmet());

// Cho phép Frontend ở domain khác gọi API
app.use(cors({
    origin: 'https://vibe-coder-frontend.com', // Chỉ cho phép domain này gọi
    credentials: true
}));

app.use(express.json());

module.exports = app;

3. File server.js: Nghệ Thuật "Hạ Cánh Mềm" (Graceful Shutdown)

Đây là nơi đẳng cấp Vibe Coder tỏa sáng. File server.js sẽ import app.js và khởi chạy nó. NHƯNG, chúng ta phải dạy cho Server cách "chết" một cách thanh lịch.

Khi bạn gõ Ctrl + C hoặc khi PM2/Docker ra lệnh restart hệ thống (gửi tín hiệu SIGINT hoặc SIGTERM), nếu theo cách code thông thường, Server sẽ "tắt điện" ngay lập tức. Những user đang thanh toán dở, đang upload file dở sẽ bị văng lỗi Connection Reset chửi thề om sòm.

Graceful Shutdown là kỹ thuật: "Ê Server, dừng nhận request MỚI đi. Chờ tao xử lý nốt mấy request CŨ đang chạy dở, đóng kết nối Database an toàn rồi hẵng chết nhé!".

// server.js
const http = require('http');
const app = require('./app');
const mongoose = require('mongoose'); // Giả sử dùng MongoDB

const PORT = process.env.PORT || 3000;

// Khởi tạo HTTP server bọc lấy Express app
const server = http.createServer(app);

// Chạy server
server.listen(PORT, () => {
    console.log(`🚀 Vibe Coder Server đang lướt trên port ${PORT}`);
});

// --- NGHỆ THUẬT HẠ CÁNH MỀM (GRACEFUL SHUTDOWN) ---

// Bắt tín hiệu ngắt từ Terminal (Ctrl + C) hoặc Docker/PM2
const shutdown = () => {
    console.log('\n⚠️ Nhận được lệnh tắt. Đang hạ cánh mềm...');

    // 1. Dừng nhận thêm request mới vào Port 3000
    server.close(async (err) => {
        if (err) {
            console.error('Lỗi khi đóng server:', err);
            process.exit(1);
        }
        
        console.log('✅ Đã đóng cổng mạng, không nhận request mới.');

        // 2. Ngắt kết nối Database an toàn (nếu có)
        try {
            // await mongoose.connection.close(); 
            // await redis.quit();
            console.log('✅ Đã ngắt kết nối Database.');
            
            console.log('💤 Chúc ngủ ngon!');
            process.exit(0); // 3. Tắt process Node.js an toàn
        } catch (dbError) {
            console.error('❌ Lỗi khi ngắt DB:', dbError);
            process.exit(1);
        }
    });

    // Ép chết nếu xử lý dở dang quá lâu (Ví dụ: 10 giây)
    setTimeout(() => {
        console.error('🚨 Quá thời gian hạ cánh (10s). Ép buộc tắt!');
        process.exit(1);
    }, 10000);
};

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);

Lời kết

Vậy là xong! Không còn là một script "Hello World" ngây ngô nữa. Server của bạn bây giờ đã có tính module cao (tách app/server), được bọc giáp chống đạn (Helmet, CORS) và biết cách hành xử vô cùng chuyên nghiệp khi bị tắt đột ngột (Graceful Shutdown). Đây chính là bộ khung chuẩn mực mà bạn có thể dùng làm Boilerplate cho mọi dự án Backend từ nay về sau.

Chủ đề tiếp theo: Rừng Rậm Định Tuyến (Routing) - Thiết Kế Cấu Trúc Thư Mục Chuẩn Clean Architecture

Server đã chạy, nhưng bây giờ bạn sẽ vứt các đoạn code xử lý logic (đăng nhập, lấy danh sách sản phẩm, thanh toán) vào đâu?

Nếu bạn vứt hàng ngàn dòng code vào chung một file app.js hoặc nhét cả SQL query thẳng vào file Router, dự án của bạn sẽ sớm biến thành một tô mì Spaghetti khổng lồ mang tên "Technical Debt" (Nợ kỹ thuật).

Ở bài viết tới, mình sẽ hướng dẫn anh em cách "chia để trị". Phân bổ code thành các tầng: Router -> Controller -> Service -> Repository. Khiến cho source code của bạn dù có lên đến 100 API thì việc tìm kiếm và sửa bug vẫn dễ như lấy kẹo trong túi. Anh em nhớ follow nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí