Back to blog

Frontend Engineer's Guide to Backend with Node.js, Bun & Deno

nodejsbackendjavascripttypescriptcareer
Frontend Engineer's Guide to Backend with Node.js, Bun & Deno

Bạn đã viết React hoặc Angular mấy năm trời. Component, state management, routing, build tool — rành hết. Nhưng mỗi lần ai nhắc đến "backend" là lại thấy như bước vào thế giới khác — server, database, middleware, deployment pipeline.

Sự thật là: bạn đã biết nhiều hơn mình tưởng. JavaScript vẫn là JavaScript. TypeScript vẫn là TypeScript. Những tư duy bạn xây dựng suốt mấy năm làm frontend chuyển thẳng sang backend luôn. Bạn không cần học ngôn ngữ mới — bạn chỉ cần một ngữ cảnh mới.

Bài viết này cho bạn lộ trình thực tế, không lý thuyết suông, để đi từ frontend engineer thành người tự tin xây dựng, deploy và bảo trì backend service.

Bạn Sẽ Học Được Gì

Kết quả học tập:
✅ Những kỹ năng frontend nào chuyển thẳng sang backend
✅ Các khái niệm cần học (và cái nào bỏ qua được)
✅ Cách chọn giữa Node.js, Bun và Deno
✅ Lộ trình từng bước từ "hello world" đến production API
✅ Những lỗi phổ biến frontend engineer hay mắc khi làm backend
✅ Cách xây dựng REST API đầu tiên từ đầu


Phần 1: Những Gì Bạn Đã Biết (Nhiều Hơn Bạn Nghĩ)

Trước khi học cái mới, hãy kiểm kê lại đã. Là frontend engineer, bạn đã có sẵn:

JavaScript / TypeScript — Vũ Khí Chính

Đây là tài sản lớn nhất. Ngôn ngữ y chang. Kiến thức của bạn về:

  • Biến, hàm, class, module
  • Promise, async/await, xử lý lỗi
  • Các method array (.map(), .filter(), .reduce())
  • Destructuring, spread operator, template literal
  • TypeScript type, interface, generic

Tất cả chuyển 1:1 sang backend. Bạn không bắt đầu từ số 0.

Quản Lý Package

Bạn đã biết npm (hoặc yarn, pnpm). Bạn đã xử lý package.json, lockfile, xung đột dependency, và semantic versioning. Project Node.js backend dùng y hệt bộ công cụ này.

Lập Trình Bất Đồng Bộ

Đây là chỗ phần lớn người mới học backend chật vật — nhưng bạn đã làm mấy năm rồi. Mỗi lần gọi fetch(), mỗi lần cập nhật state sau khi nhận response từ API, mỗi useEffect với cleanup function — đó chính là lập trình bất đồng bộ. Trên backend, cùng pattern đó áp dụng cho database query, đọc file, và HTTP request.

JSON Mọi Nơi

Bạn đã tư duy bằng JSON. Request body, response payload, file cấu hình — format dữ liệu không thay đổi.

Build Tool và Quy Trình Dev

Webpack, Vite, esbuild, ESLint, Prettier, CI/CD pipeline — bạn đã config hết rồi. Project backend dùng tooling tương tự (thường còn đơn giản hơn).


Phần 2: Thực Sự Cần Học Gì Mới

Giờ hãy thành thật về những thứ bạn chưa biết. Đây là những khái niệm thật sự mới khi chuyển sang backend.

1. HTTP — Từ Phía Bên Kia

Ở frontend, bạn gửi HTTP request. Ở backend, bạn nhận chúng.

Bạn luôn là client. Giờ bạn là server. Nghĩa là cần hiểu:

  • Cách parse request đến (body, header, query param, URL param)
  • Cách gửi response (status code, header, JSON body)
  • Routing hoạt động thế nào trên server (không phải React Router — mà là mapping URL → handler thực sự)
Góc Nhìn FrontendGóc Nhìn Backend
fetch('/api/users')Nhận request tại /api/users
Đọc response.json()Viết res.json({ users })
Xử lý loading/error stateXử lý validation/auth/error
Một user tại một thời điểmHàng nghìn request đồng thời

2. Database — Vùng Đất Mới

Đây là khoảng trống kiến thức lớn nhất với hầu hết frontend engineer. Ở frontend, data đến từ API. Ở backend, bạn là người lấy data từ database.

Bạn cần học:

  • SQL cơ bản: SELECT, INSERT, UPDATE, DELETE, JOIN
  • ORM: Prisma, Drizzle, hoặc TypeORM (biến SQL thành TypeScript)
  • Thiết kế database: Bảng, quan hệ, index, migration
  • Quản lý kết nối: Connection pool, connection string

Tin vui: Nếu bạn đã dùng Prisma với Next.js, bạn đã có lợi thế rồi.

3. Authentication & Authorization

Ở frontend, bạn lưu token và gửi kèm request. Ở backend, bạn:

  • Xác thực token đó
  • Hash mật khẩu (không bao giờ lưu plain text)
  • Quản lý session hoặc JWT
  • Quyết định ai được truy cập gì (role, permission)

4. Truy Cập File System

Code frontend chạy trong sandbox — không truy cập file được. Code backend có thể đọc ghi file, tạo thư mục, stream file lớn, và theo dõi thay đổi file.

// Cái này bất khả thi trên browser, bình thường trên backend
import { readFile, writeFile } from 'node:fs/promises';
 
const data = await readFile('./config.json', 'utf-8');
const config = JSON.parse(data);
 
config.lastUpdated = new Date().toISOString();
await writeFile('./config.json', JSON.stringify(config, null, 2));

5. Environment & Bảo Mật

Ở frontend, mọi thứ đều public — user có thể xem source code. Ở backend:

  • Secret giữ được bí mật (API key, mật khẩu database)
  • Biến môi trường (file .env) chứa cấu hình
  • Validate input là bắt buộc (đừng bao giờ tin user input)
  • Bạn xử lý dữ liệu nhạy cảm (thông tin cá nhân, thanh toán)

Phần 3: Chọn Runtime Nào

Bạn có 3 lựa chọn chính để chạy JavaScript trên server. Cả 3 chạy cùng ngôn ngữ, nhưng khác nhau về triết lý và tính năng.

Node.js — Chuẩn Mực Lâu Năm

  • Tạo bởi: Ryan Dahl, 2009
  • Engine: V8 (giống Chrome)
  • Package manager: npm (registry lớn nhất thế giới)
  • Ecosystem: Khổng lồ — Express, Fastify, NestJS, hàng triệu package
  • Khi nào chọn: Làm production, nhu cầu thị trường việc làm, support thư viện tối đa
# Cài đặt và chạy
node --version
node server.js
 
# Hoặc với TypeScript
npx tsx server.ts

Bun — Tân Binh Tốc Độ

  • Tạo bởi: Jarred Sumner, 2022
  • Engine: JavaScriptCore (giống Safari)
  • Package manager: Tích hợp sẵn (nhanh hơn npm)
  • Ecosystem: Tương thích Node.js (hầu hết npm package chạy được)
  • Khi nào chọn: Project cần tốc độ, tooling all-in-one, project mới
# Cài đặt và chạy
bun --version
bun run server.ts  # Hỗ trợ TypeScript native, không cần config

Deno — Lựa Chọn An Toàn

  • Tạo bởi: Ryan Dahl, 2018 (đúng vậy, cùng tác giả Node.js)
  • Engine: V8
  • Package manager: Import URL + deno.json imports map
  • Ecosystem: Đang phát triển, có lớp tương thích Node.js
  • Khi nào chọn: Project ưu tiên bảo mật, module system sạch, web standard
# Cài đặt và chạy
deno --version
deno run --allow-net --allow-read server.ts

Nên Chọn Cái Nào?

Tiêu chíNode.jsBunDeno
Thị trường việc làm🟢 Chiếm ưu thế🟡 Đang phát triển🟡 Ngách
Tài liệu học🟢 Phong phú🟡 Trung bình🟡 Trung bình
Tốc độ🟡 Tốt🟢 Nhanh nhất🟢 Nhanh
TypeScript🟡 Cần setup🟢 Native🟢 Native
Tương thích npm🟢 Hoàn toàn🟢 Gần hoàn toàn🟡 Qua compat layer
Mô hình bảo mật🟡 Mở mặc định🟡 Mở mặc định🟢 Permission-based

Khuyến nghị cho frontend engineer: Bắt đầu với Node.js. Nhiều tutorial nhất, cộng đồng lớn nhất, và nhiều job listing nhất. Khi đã thoải mái, thử Bun hoặc Deno — chuyển đổi giữa chúng rất đơn giản vì code gần như giống nhau.


Phần 4: Lộ Trình Học

Đây là kế hoạch từng tuần để đi từ frontend engineer thành backend-capable developer. Không cần nghỉ việc — 1-2 tiếng mỗi ngày là đủ.

Tuần 1-2: Node.js Cơ Bản

Mục tiêu: Hiểu cách Node.js hoạt động và xây HTTP server cơ bản.

// server.js — Server backend đầu tiên của bạn
import { createServer } from 'node:http';
 
const server = createServer((req, res) => {
  if (req.url === '/api/hello' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'Hello from the backend!' }));
    return;
  }
 
  res.writeHead(404);
  res.end('Not found');
});
 
server.listen(3001, () => {
  console.log('Server running on http://localhost:3001');
});

Cần học:

  • Module tích hợp Node.js: http, fs, path, crypto
  • Event loop (bạn đã biết từ browser JS — cùng khái niệm, khác API)
  • CommonJS vs ESM (dùng ESM — bạn đã biết từ frontend)
  • Đọc biến môi trường với process.env

Tuần 3-4: Chọn Framework

http.createServer() thuần tốt để học nhưng không thực tế. Chọn framework:

Express.js (khuyến nghị cho việc học):

import express from 'express';
 
const app = express();
app.use(express.json());
 
const users = [];
 
app.get('/api/users', (req, res) => {
  res.json(users);
});
 
app.post('/api/users', (req, res) => {
  const { name, email } = req.body;
 
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
 
  const user = { id: Date.now(), name, email };
  users.push(user);
  res.status(201).json(user);
});
 
app.listen(3001, () => {
  console.log('API running on http://localhost:3001');
});

Các framework khác nên biết:

FrameworkPhong cáchPhù hợp cho
ExpressTối giản, middleware-basedHọc, API nhỏ
FastifyTập trung hiệu năngAPI thông lượng cao
NestJSFull framework, lấy cảm hứng từ AngularApp enterprise (quen thuộc nếu biết Angular!)
HonoSiêu nhẹ, multi-runtimeEdge function, Bun/Deno/Cloudflare

Nếu bạn là Angular developer, NestJS sẽ quen như ở nhà — dùng decorator, dependency injection, module, và cấu trúc project tương tự. Quá trình chuyển đổi mượt mà đáng ngạc nhiên.

Tuần 5-6: Tích Hợp Database

Đã đến lúc thay array trong memory bằng database thật.

Lộ trình khuyến nghị: PostgreSQL + Prisma ORM

npm install prisma @prisma/client
npx prisma init

Đị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())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}
 
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
}

Dùng trong API:

import { PrismaClient } from '@prisma/client';
 
const prisma = new PrismaClient();
 
// GET /api/users
app.get('/api/users', async (req, res) => {
  const users = await prisma.user.findMany({
    include: { posts: true },
  });
  res.json(users);
});
 
// POST /api/users
app.post('/api/users', async (req, res) => {
  try {
    const user = await prisma.user.create({
      data: { name: req.body.name, email: req.body.email },
    });
    res.status(201).json(user);
  } catch (error) {
    if (error.code === 'P2002') {
      return res.status(409).json({ error: 'Email already exists' });
    }
    res.status(500).json({ error: 'Internal server error' });
  }
});

Cái này sẽ quen lắm — API của Prisma đọc như TypeScript, không phải SQL. Bạn có đầy đủ autocompletion và type safety.

Tuần 7-8: Authentication & Middleware

Thêm JWT authentication để bảo vệ route:

import jwt from 'jsonwebtoken';
 
// Middleware — nghĩ như higher-order component của React, nhưng cho route
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
 
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next(); // Chuyển sang handler tiếp theo
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}
 
// Route được bảo vệ
app.get('/api/profile', authenticate, async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: req.user.id },
  });
  res.json(user);
});

Khái niệm quan trọng cho frontend dev: Middleware giống chuỗi hàm xử lý request trước khi đến route handler. Nghĩ như pipeline — tương tự RxJS pipe trong Angular hoặc middleware trong Redux.

Tuần 9-10: Testing & Xử Lý Lỗi

Viết test cho API. Nếu bạn đã dùng Jest hoặc Vitest ở frontend, công cụ test y chang:

// users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from './app';
 
describe('Users API', () => {
  it('should create a new user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Jane', email: '[email protected]' });
 
    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty('id');
    expect(response.body.name).toBe('Jane');
  });
 
  it('should return 400 for missing fields', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'Jane' });
 
    expect(response.status).toBe(400);
  });
});

Tuần 11-12: Deployment

Deploy API ra thế giới thực. Các lựa chọn:

Nền tảngĐộ khóChi phíPhù hợp cho
RailwayDễFree tierPrototype nhanh
RenderDễFree tierSide project
Fly.ioTrung bìnhFree tierPhân phối toàn cầu
AWS/GCP/AzureKhóPay-as-you-goProduction quy mô lớn
Docker + VPSTrung bình$5-10/thángToàn quyền kiểm soát

Checklist deployment tối thiểu:

✅ Biến môi trường đã cấu hình (không có secret trong code)
✅ Connection string database đã set
✅ CORS đã cấu hình cho domain frontend
✅ Xử lý lỗi không lộ stack trace
✅ Endpoint health check (GET /health) hoạt động
✅ Logging đã setup để debug lỗi production


Phần 5: Lỗi Phổ Biến Frontend Engineer Hay Mắc

Lỗi 1: Coi Server Như Browser

// ❌ SAI: State toàn cục có thể thay đổi (chạy trên browser ok, thảm họa trên server)
let currentUser = null;
 
app.post('/api/login', (req, res) => {
  currentUser = req.body.user; // Chia sẻ giữa TẤT CẢ request!
  res.json({ success: true });
});
 
// ✅ ĐÚNG: Mỗi request là độc lập
app.post('/api/login', (req, res) => {
  const user = req.body.user; // Phạm vi chỉ trong request này
  const token = generateToken(user);
  res.json({ token });
});

Ở frontend, bạn có một user. Ở backend, bạn có hàng nghìn user đồng thời chia sẻ cùng process. Đừng lưu data per-request vào biến module-level.

Lỗi 2: Không Xử Lý Lỗi Đúng Cách

// ❌ SAI: Lỗi không xử lý làm crash server
app.get('/api/users/:id', async (req, res) => {
  const user = await prisma.user.findUnique({ where: { id: +req.params.id } });
  res.json(user); // Crash nếu user null hoặc id không phải số
});
 
// ✅ ĐÚNG: Xử lý mọi trường hợp lỗi
app.get('/api/users/:id', async (req, res) => {
  try {
    const id = parseInt(req.params.id);
    if (isNaN(id)) {
      return res.status(400).json({ error: 'Invalid user ID' });
    }
 
    const user = await prisma.user.findUnique({ where: { id } });
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
 
    res.json(user);
  } catch (error) {
    console.error('Failed to fetch user:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

Ở frontend, lỗi hiện màn hình trắng. Ở backend, lỗi không xử lý sập luôn cả server cho tất cả user.

Lỗi 3: Block Event Loop

// ❌ SAI: Đọc file đồng bộ block TẤT CẢ request
import { readFileSync } from 'node:fs';
 
app.get('/api/report', (req, res) => {
  const data = readFileSync('./large-report.csv', 'utf-8'); // Block!
  res.json(processReport(data));
});
 
// ✅ ĐÚNG: Dùng thao tác bất đồng bộ
import { readFile } from 'node:fs/promises';
 
app.get('/api/report', async (req, res) => {
  const data = await readFile('./large-report.csv', 'utf-8'); // Không block
  res.json(processReport(data));
});

Lỗi 4: Quên Validate Input

Ở frontend, bạn validate cho UX. Ở backend, bạn validate cho bảo mật.

// ❌ SAI: Tin tưởng user input
app.post('/api/users', async (req, res) => {
  const user = await prisma.user.create({ data: req.body }); // Nhận bất cứ gì!
  res.json(user);
});
 
// ✅ ĐÚNG: Validate tất cả
import { z } from 'zod';
 
const CreateUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
});
 
app.post('/api/users', async (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
 
  if (!result.success) {
    return res.status(400).json({ errors: result.error.flatten() });
  }
 
  const user = await prisma.user.create({ data: result.data });
  res.status(201).json(user);
});

Bonus: Bạn có thể chia sẻ Zod schema giữa frontend và backend — validate một lần, dùng mọi nơi.

Lỗi 5: Không Hiểu Concurrency

// ❌ SAI: Tư duy tuần tự khi request chồng chéo
let counter = 0;
 
app.post('/api/increment', async (req, res) => {
  counter++; // Race condition với request đồng thời!
  res.json({ counter });
});
 
// ✅ ĐÚNG: Dùng thao tác atomic trong database
app.post('/api/increment', async (req, res) => {
  const result = await prisma.counter.update({
    where: { id: 1 },
    data: { value: { increment: 1 } },
  });
  res.json({ counter: result.value });
});

Phần 6: Project Thực Hành Đầu Tiên

Xây Bookmark API — backend service cho phép user lưu và tổ chức link. Project này cover mọi khái niệm trong lộ trình.

Tính năng:

  • Đăng ký và đăng nhập (JWT auth)
  • CRUD bookmark
  • Tổ chức theo tag
  • Chức năng tìm kiếm
  • Validate input với Zod

Tech Stack:

  • Runtime: Node.js
  • Framework: Express.js
  • Database: PostgreSQL + Prisma
  • Auth: JWT + bcrypt
  • Validation: Zod
  • Testing: Vitest + Supertest

Cấu Trúc Project:

bookmark-api/
├── src/
│   ├── routes/
│   │   ├── auth.ts          # Login, register
│   │   ├── bookmarks.ts     # CRUD cho bookmark
│   │   └── tags.ts          # Quản lý tag
│   ├── middleware/
│   │   ├── auth.ts          # Xác thực JWT
│   │   ├── validate.ts      # Validate Zod
│   │   └── errorHandler.ts  # Xử lý lỗi toàn cục
│   ├── schemas/
│   │   └── bookmark.ts      # Zod schema
│   ├── app.ts               # Setup Express app
│   └── server.ts            # Entry point server
├── prisma/
│   └── schema.prisma        # Schema database
├── tests/
│   └── bookmarks.test.ts    # Test API
├── .env                     # Biến môi trường
├── package.json
└── tsconfig.json

Cấu trúc này sẽ rất quen thuộc nếu bạn đã làm Angular hoặc React project có tổ chức tốt. Pattern tách concern thành route, middleware, và schema map trực tiếp sang cách bạn tổ chức component, guard, và service trong frontend app.


Tổng Kết

✅ Bạn không bắt đầu từ số 0 — JavaScript, TypeScript, async pattern, npm, và kiến thức JSON chuyển thẳng sang
✅ Những khái niệm mới chính là: HTTP từ phía server, database, authentication, và truy cập file system
✅ Bắt đầu với Node.js + Express — ecosystem lớn nhất và tài liệu học nhiều nhất
✅ NestJS giống Angular — nếu bạn là Angular dev, chuyển sang NestJS mượt mà đáng ngạc nhiên
✅ Dùng Prisma cho database — biến SQL thành TypeScript với đầy đủ autocompletion
✅ Mỗi request là độc lập — đừng dùng state toàn cục có thể thay đổi trên server
✅ Validate mọi input — validate frontend cho UX, validate backend cho bảo mật
✅ Xây project thật — lý thuyết chỉ đưa bạn đến một mức, hãy xây API hoàn chỉnh end-to-end
✅ Khoảng cách nhỏ hơn bạn nghĩ — hầu hết frontend engineer có thể xây production API trong 2-3 tháng


Đọc Thêm

Nếu muốn tìm hiểu sâu hơn các chủ đề trong bài:


Tiếp Theo Là Gì?

Bạn không cần ai cho phép để trở thành "backend engineer." Bạn là JavaScript developer — backend chỉ là một phòng khác trong cùng ngôi nhà.

Chọn một thứ từ lộ trình này, dành một tiếng cho nó hôm nay, và tiếp tục ngày mai. Vài tuần nữa, bạn sẽ tự hỏi sao không làm sớm hơn.

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