Đi tới nội dung

Backend Engineer

Thiết kế, xây dựng và vận hành API, dữ liệu và service phía sau mỗi sản phẩm

Lộ trình ưu tiên Node.js và TypeScript, đi từ nền tảng HTTP tới vận hành service phân tán trên production, với các khái niệm dùng được cho mọi ngôn ngữ. Dành cho dev frontend muốn sang backend và backend junior muốn đào sâu thiết kế API, PostgreSQL, auth, xử lý bất đồng bộ, testing và vận hành.

Bắt đầu từ chặng 01
chặng
10chặng
chủ đề
52chủ đề
tuần, học part-time
~37tuần, học part-time
  • Nền tảng
  • Cốt lõi
  • Nâng cao
  • Node.jsNode.js
  • TypeScriptTypeScript
  • NestJSNestJS
  • PostgreSQLPostgreSQL
  • PrismaPrisma
  • RedisRedis
  • OpenTelemetry
  • DockerDocker

Góc nhìn của mình

Mình là dev frontend, không phải backend engineer chuyên trách, nhưng vài năm qua mình đã làm các service backend với NestJS, Prisma và PostgreSQL ở mức làm việc được. Bước qua ranh giới đó thay đổi cả cách mình viết frontend: mình thôi coi API là hộp đen. Map này là lộ trình mình ghép lại từ trải nghiệm đó, những lỗ hổng kiến thức mình gặp, và thứ tự mình sẽ đi nếu nghiêm túc bắt đầu backend từ hôm nay. Ở những phần mình chưa có chiều sâu production, nhất là scaling và hệ phân tán, mình dựa vào tài liệu chính thức và sách uy tín chứ không nhận là đã có kinh nghiệm.

Điều mình ước biết sớm hơn

  • Database mới là sản phẩm. Thời gian bỏ vào mô hình dữ liệu, index và transaction đáng giá hơn nhiều so với học thêm một framework.
  • Authentication phần lớn đã được giải quyết; authorization mới là nơi bug thật nằm, nên hãy kiểm tra quyền sở hữu trên mọi object bạn trả về.
  • Mọi lời gọi mạng đều có thể lỗi, timeout hoặc bị retry, nên hãy thiết kế idempotency từ endpoint đầu tiên, đừng đợi tới lần thanh toán trùng đầu tiên.
  • Mock làm test của mình xanh còn query thì sai. Test trên PostgreSQL thật chạy trong container là bước ngoặt để mình tin vào code của mình.
Đã qua 0/10 chặng

Nền tảng01–03

  1. 01Chặng 01, 2 tuầnNền tảng HTTP và networkingGiải thích được chuyện gì xảy ra trên đường truyền từ lúc DNS lookup tới khi có response, và debug một request bằng curl lẫn tab Network của trình duyệt.
    Hồi làm frontend mình coi fetch như hộp đen, trả JSON hoặc throw lỗi. Lúc chuyển qua backend, phần lớn bug ban đầu của mình thật ra là bug HTTP: status code sai, thiếu cache header, CORS cấu hình sai phía server chứ không phải phía client.

    Kiến thức cần nắm

    Đường đi của một request
    DNS resolution, TCP handshake, TLS 1.3 handshake rồi mới tới trao đổi HTTP. Cần biết độ trễ đến từ đâu (số round trip, tái sử dụng kết nối qua keep-alive) và vì sao khi reverse proxy hay load balancer terminate TLS thì app của bạn thấy khác đi, ví dụ X-Forwarded-For và IP thật của client.
    Ngữ nghĩa HTTP
    Các method và tính safe, idempotent của chúng (GET, PUT, DELETE là idempotent, POST thì không), các nhóm status code, và khác biệt giữa 401 với 403 hay 400 với 422. RFC 9110 hiện là nguồn duy nhất cho phần ngữ nghĩa, tách khỏi các phiên bản giao thức trên đường truyền.
    HTTP/1.1, HTTP/2 và HTTP/3
    HTTP/2 multiplex nhiều stream trên một kết nối TCP, HTTP/3 chạy trên QUIC qua UDP và loại bỏ head-of-line blocking của TCP. Thực tế service Node của bạn thường nói HTTP/1.1 sau một proxy hoặc CDN, còn proxy đó nói HTTP/2 hay HTTP/3 với client.
    Cache và conditional request
    Cache-Control (max-age, s-maxage, no-store, private), ETag kèm If-None-Match, và Vary. Đặt đúng các header này cho API response thì CDN và trình duyệt gánh bớt tải trước khi nó chạm tới database.
    CORS, cookie và các header quan trọng
    CORS do trình duyệt thực thi, server chỉ khai báo policy qua Access-Control-Allow-*; request không phải simple request sẽ có preflight OPTIONS. Các thuộc tính cookie HttpOnly, Secure, SameSite và prefix __Host- quyết định phần lớn độ an toàn của session.

    Nên làm

    • Tái hiện mọi bug bằng một lệnh curl -v thô trước khi sửa code.
    • Chọn status code có chủ đích và giữ một bảng ngắn liệt kê chúng trong README của dự án.
    • Đặt Cache-Control rõ ràng cho mọi API response, kể cả khi giá trị là no-store.
    • Chỉ tin forwarded header từ proxy bạn kiểm soát và cấu hình trust đó một cách tường minh.

    Dễ sai

    • Trả 200 kèm một field error trong body, làm hỏng cache, retry và monitoring.
    • Sửa CORS bằng cách cho phép mọi origin kèm credentials thay vì liệt kê đúng các origin thật.

    Project chốt chặng: HTTP inspector tự viết

    Viết một server nhỏ chỉ dùng node:http (không framework) trả lại method, header và body, hỗ trợ ETag với response 304, và cấu hình CORS đúng cho một origin được phép. Xong khi curl -v cho ra đúng 200/304/405 và preflight từ trình duyệt ở origin khác chỉ thành công với origin được phép.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Cơ bảnClient bị timeout và tự động retry. Với request nào việc retry mù quáng là không an toàn nếu không có cơ chế bảo vệ thêm?
    2. Câu 2/4Cơ bảnMột user đã đăng nhập hợp lệ gọi endpoint chỉ dành cho admin. Status code phù hợp là gì?
    3. Câu 3/4Cơ bảnFrontend ở https://app.example.com gọi API kèm cookie. Server trả Access-Control-Allow-Origin: * cùng Access-Control-Allow-Credentials: true nhưng browser vẫn chặn. Cách sửa đúng là gì?
    4. Câu 4/4Cơ bảnApp chạy sau load balancer và log mọi request đều đến từ 10.0.0.5. Cách sửa đúng là gì?
  2. 02Chặng 02, 3 tuầnHiểu sâu runtime Node.jsHiểu event loop, stream và worker thread đủ để giữ service phản hồi tốt khi tải cao.
    Mình biết async/await từ code React, nhưng chưa thật sự hiểu cái gì chặn event loop cho tới khi một vòng lặp nặng CPU làm đứng mọi request trong một side project. Học kỹ runtime là lúc Node hết là phép màu và trở thành công cụ.

    Kiến thức cần nắm

    Các phase của event loop
    Timers, pending callbacks, poll, check (setImmediate) và close, cộng với microtask queue (promise, queueMicrotask) và process.nextTick chạy xen giữa. Hiểu vì sao chỉ một lệnh JSON.parse đồng bộ trên payload lớn cũng làm khựng mọi request đang chạy song song, và đo nó bằng event loop delay trong perf_hooks.
    Stream và backpressure
    Readable, Writable, Transform, Duplex, stream.pipeline và async iteration với for await. Backpressure (write() trả về false, sự kiện drain) là thứ giữ cho upload file hay export CSV không nạp hết mọi thứ vào memory.
    Worker thread và libuv pool
    worker_threads cho việc nặng CPU như xử lý ảnh hay hash, khác với libuv thread pool vốn đã lo fs, dns.lookup và crypto. Muốn dùng nhiều core để xử lý request thì cách chính vẫn là cluster hoặc chạy nhiều process sau load balancer.
    Tính năng Node hiện đại
    Node 24 là dòng LTS đang active và type stripping TypeScript gốc đã stable từ bản 24.12, nhưng nó chỉ bỏ phần cú pháp có thể xóa: enum, parameter property và decorator metadata không được hỗ trợ, điều này ảnh hưởng tới NestJS. fetch có sẵn, node:test, --watch và --env-file giúp bớt nhiều dev dependency.
    Bun và Deno, nói ngắn
    Bun là runtime kiêm package manager và test runner rất nhanh, tương thích Node cao; Deno tập trung vào permission an toàn mặc định và API chuẩn web, có hỗ trợ npm. Nên học Node trước vì hệ sinh thái production, tài liệu và hosting vẫn mặc định là Node, rồi đánh giá hai cái kia theo từng dự án.

    Nên làm

    • Dùng stream.pipeline thay vì tự nối pipe để lỗi và cleanup được lan truyền đúng.
    • Theo dõi event loop delay và heap usage ngay từ ngày đầu.
    • Đưa việc nặng CPU sang worker thread hoặc một queue worker riêng, không bao giờ làm trực tiếp trong request handler.
    • Xử lý unhandledRejection và SIGTERM tường minh, shutdown một cách graceful.

    Dễ sai

    • Đọc cả file upload vào một Buffer trong khi dùng stream thì memory gần như cố định.
    • Nghĩ rằng code async là chạy CPU song song; thật ra nó chỉ chồng thời gian chờ I/O lên nhau.

    Project chốt chặng: CSV exporter dạng stream

    Làm một endpoint stream một triệu dòng dữ liệu sinh ra dưới dạng CSV và một endpoint khác tính hash mật khẩu trong worker thread. Xong khi memory giữ phẳng trong lúc export, event loop delay vẫn thấp khi đang hash, và server tắt sạch khi nhận SIGTERM mà không rớt request đang xử lý.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Cơ bảnVì sao một lệnh JSON.parse trên payload 200MB làm chậm mọi request đang chạy đồng thời?
    2. Câu 2/4Cơ bảnHandler chạy await Promise.all(images.map((img) => resizeInJs(img))), trong đó resizeInJs là hàm async nhưng toàn bộ là tính toán JavaScript. Điều gì xảy ra?
    3. Câu 3/4Cơ bảnEndpoint cần trả về file CSV 2GB lấy từ database. Cách nào đúng?
    4. Câu 4/4Cơ bảnTeam muốn chạy app NestJS bằng node src/main.ts nhờ type stripping có sẵn trong Node. Điều gì sẽ hỏng?
  3. 03Chặng 03, 3 tuầnThiết kế API bền vữngThiết kế một API nhất quán, có tài liệu, lỗi rõ ràng, retry an toàn, phân trang ổn định và chính sách versioning.
    Là dev frontend, mình từng là người dùng API khổ sở vì response mỗi chỗ một kiểu và breaking change âm thầm. Chính nỗi khổ đó là người thầy tốt nhất: hãy thiết kế cái API mà ngày xưa bạn ước được nhận.

    Kiến thức cần nắm

    Mô hình resource và quy ước REST
    Dùng danh từ cho resource, sub-resource để thể hiện quan hệ sở hữu, đặt tên và casing nhất quán, filter, sort và sparse field qua query parameter. Dùng RFC 9457 Problem Details (application/problem+json) làm định dạng lỗi duy nhất để client xử lý lỗi một cách tổng quát.
    OpenAPI là hợp đồng
    OpenAPI 3.1 đã đồng bộ schema với JSON Schema, còn 3.2 (ra năm 2025) thêm media type dạng stream như SSE và JSON Lines, tag phân cấp và method QUERY. Chọn code-first (sinh spec từ decorator hoặc Zod schema) hay design-first đều được, nhưng luôn sinh typed client và lint spec trong CI.
    Idempotency và retry an toàn
    Client sẽ retry khi timeout, nên các endpoint POST tạo thanh toán hay đơn hàng cần header Idempotency-Key (một IETF draft theo đúng cách Stripe đã phổ biến). Lưu key cùng hash của request và response, trả lại kết quả cũ khi gửi lặp, và từ chối nếu cùng key nhưng body khác.
    Phân trang
    Offset pagination đơn giản nhưng chậm và không ổn định trên bảng lớn hoặc dữ liệu thay đổi liên tục. Cursor hay keyset pagination (WHERE (created_at, id) < ($1, $2) ORDER BY created_at DESC, id DESC) ổn định và tận dụng được index; trả về một cursor mờ (opaque) cùng link next.
    Versioning và tiến hóa API
    Ưu tiên thay đổi kiểu bổ sung, tương thích ngược; khi buộc phải breaking thì version qua URL path hoặc header và báo trước việc gỡ bỏ bằng header Deprecation và Sunset. Contract test và changelog biến deprecation thành một quy trình chứ không phải bất ngờ.
    Đánh đổi giữa GraphQL và tRPC
    GraphQL hợp khi có nhiều client với nhu cầu dữ liệu khác nhau, nhưng kéo theo rủi ro N+1 (dùng DataLoader), giới hạn độ phức tạp query và khó cache HTTP hơn. tRPC cho type end-to-end gần như không tốn công trong monorepo TypeScript, nhưng không phải hợp đồng API public; REST kèm OpenAPI vẫn là lựa chọn mặc định an toàn cho bên ngoài.

    Nên làm

    • Viết OpenAPI spec hoặc schema trước khi implement và review nó như review code.
    • Trả về một định dạng lỗi duy nhất ở mọi nơi, có type ổn định cho máy đọc và detail cho người đọc.
    • Bắt buộc idempotency key cho mọi endpoint không idempotent liên quan tới tiền hoặc tạo bản ghi quan trọng.
    • Mặc định dùng cursor pagination và giới hạn page size tối đa ở phía server.

    Dễ sai

    • Để lộ tên cột database và ID nội bộ thẳng ra hợp đồng public.
    • Ship breaking change mà giữ nguyên version chỉ vì đang có đúng một client biết đến.

    Project chốt chặng: Hợp đồng API cho Orders

    Thiết kế OpenAPI spec cho một service đơn hàng gồm list (cursor pagination), create (idempotency key) và lỗi dạng Problem Details, rồi sinh typed client TypeScript từ spec. Xong khi spec qua được linter, client compile được với spec, và gửi lại lệnh create với cùng key thì nhận về đúng đơn hàng ban đầu.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Cơ bảnVì sao keyset pagination tốt hơn offset trên bảng lớn và thay đổi liên tục?
    2. Câu 2/4Cơ bảnVì sao trả về 200 kèm { "error": "not found" } trong body là một thiết kế tệ?
    3. Câu 3/4Cơ bảnClient gửi POST /payments với Idempotency-Key: k1 và amount 100, sau đó gửi lại cùng key k1 nhưng amount 500. Server nên làm gì?
    4. Câu 4/4Cơ bảnCần đổi field user_name thành username trong API public. Cách nào đúng?

Cốt lõi04–08

  1. 04Chặng 04, 3 tuầnKiến trúc NestJSTổ chức một service NestJS thành các module gắn kết, có request lifecycle rõ ràng, validation chặt và provider dễ test.
    NestJS là cầu nối đưa mình từ lối nghĩ frontend kiểu Angular sang backend, và cũng là chỗ mình có nhiều thời gian thực hành nhất. Cái sai hồi đầu của mình là nhét business logic vào controller và tạo một shared module khổng lồ.

    Kiến thức cần nắm

    Module và ranh giới
    Tổ chức theo feature hoặc domain (orders, billing, users), chỉ export những gì module khác cần, và tránh import vòng thay vì giấu nó bằng forwardRef. Dynamic module (forRoot, forRootAsync, ConfigurableModuleBuilder) là cách làm phần hạ tầng tái sử dụng như module database hay mail.
    Dependency injection
    Provider mặc định là singleton; scope request và transient lan truyền theo chuỗi và tốn hiệu năng, nên ưu tiên AsyncLocalStorage (ví dụ qua nestjs-cls) cho context theo từng request. Custom provider với useFactory, useClass và injection token giúp thay implementation khi test.
    Request lifecycle
    Middleware, rồi guard (quyết định phân quyền), interceptor phần trước, pipe (validate và transform), handler, interceptor phần sau, và exception filter. Nắm thứ tự này là biết mỗi mối quan tâm thuộc về đâu: auth ở guard, log và đo thời gian ở interceptor, map lỗi ở filter.
    Validation và DTO
    ValidationPipe toàn cục với class-validator và class-transformer, bật whitelist và forbidNonWhitelisted để loại field lạ, hoặc dùng Zod schema qua custom pipe nếu muốn một schema vừa validate runtime vừa sinh type. Validate ở biên và để domain service làm việc với dữ liệu đã được tin cậy.
    NestJS 11 trong thực tế
    NestJS 11 yêu cầu Node 20 trở lên và mặc định dùng Express 5, Fastify là adapter thay thế khi cần throughput cao hơn. Vì Nest dựa vào decorator metadata được emit, hãy build bằng tsc hoặc SWC thay vì trông vào type stripping có sẵn của Node.

    Nên làm

    • Giữ controller mỏng: chỉ map HTTP sang lời gọi service và ngược lại.
    • Bật ValidationPipe toàn cục với whitelist và transform ngay từ commit đầu tiên.
    • Nạp và validate config lúc khởi động, thiếu biến môi trường là fail ngay.
    • Dùng injection token và interface cho service bên ngoài để test có thể cung cấp bản giả.

    Dễ sai

    • Chuyển provider sang request scope chỉ để đọc user hiện tại, làm chậm mọi request trong chuỗi phụ thuộc.
    • Catch lỗi ở mọi service rồi trả null thay vì throw domain error và để filter map ra HTTP.

    Project chốt chặng: Service NestJS chia module

    Implement API orders từ stage trước bằng NestJS với các module orders, catalog và health tách riêng, một validation pipe toàn cục, một logging interceptor và một exception filter trả lỗi dạng Problem Details. Xong khi input sai trả 400 kèm lỗi từng field, field lạ bị từ chối, và mỗi module unit test được với provider giả.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpMột route NestJS có middleware, guard, interceptor và ValidationPipe. Trước khi tới handler, chúng chạy theo thứ tự nào?
    2. Câu 2/4Trung cấpVì sao không nên đặt một provider thành request-scoped chỉ để đọc current user?
    3. Câu 3/4Trung cấpCreateUserDto chỉ có email và name. Attacker gửi thêm "isAdmin": true. Cấu hình ValidationPipe nào trả lỗi 400 thay vì âm thầm bỏ field đó?
    4. Câu 4/4Trung cấpOrderService bắt mọi lỗi và trả về null, controller lại trả 200 với body rỗng khi không tìm thấy đơn. Cách sửa đúng?
  2. 05Chặng 05, 6 tuầnPostgreSQL và mô hình dữ liệuMô hình hóa dữ liệu trên PostgreSQL, viết và index query hiệu quả, xử lý transaction đúng và ship migration an toàn qua ORM.
    Prisma giúp mình ship feature trước khi hiểu nó sinh ra SQL gì, và sau đó mình trả giá bằng query chậm và lỗi N+1. Stage này dài nhất là có chủ đích: database sống lâu hơn mọi framework bạn đặt phía trước nó.

    Kiến thức cần nắm

    Mô hình hóa và constraint
    Chuẩn hóa trước, rồi phi chuẩn hóa có chủ đích; ép các bất biến bằng NOT NULL, CHECK, UNIQUE và foreign key chứ không chỉ trong code ứng dụng. Dùng timestamptz, numeric cho tiền, và cân nhắc uuidv7() có sẵn từ PostgreSQL 18 làm primary key có thứ tự thời gian, index tốt hơn UUID ngẫu nhiên.
    Index và query plan
    B-tree cho so sánh bằng và khoảng, thứ tự cột trong composite index, partial index và expression index, GIN cho jsonb và full text. Đọc output EXPLAIN (ANALYZE, BUFFERS) để phát hiện sequential scan, ước lượng số dòng sai và sort bị tràn ra đĩa.
    Transaction và isolation
    Read Committed là mặc định và cho phép non-repeatable read; Repeatable Read cho snapshot isolation; Serializable chặn các anomaly như write skew nhưng bạn phải retry khi gặp serialization failure. Dùng SELECT ... FOR UPDATE hoặc optimistic concurrency với cột version cho counter và tồn kho.
    Migration không downtime
    Áp dụng expand and contract: thêm cột nullable, backfill theo batch, rồi thêm constraint với NOT VALID và VALIDATE sau, tạo index bằng CONCURRENTLY. Không bao giờ đổi tên hay xóa cột trong cùng release với lúc code ngừng dùng nó.
    ORM: Prisma và Drizzle
    Prisma 7 đã thay query engine viết bằng Rust bằng engine TypeScript và giờ dùng driver adapter như @prisma/adapter-pg, nhẹ hơn và chạy được ở edge. Drizzle gần SQL hơn, schema viết bằng TypeScript và không cần codegen; dùng cái nào cũng phải biết khi nào xuống raw SQL với tham số có type.
    Kết nối và pooling
    Mỗi kết nối PostgreSQL là một process, nên cần pool trong app và dùng PgBouncer hoặc pooler managed ở transaction mode khi chạy serverless hay nhiều replica. Đặt statement_timeout và idle_in_transaction_session_timeout để một bug không giữ lock mãi mãi.

    Nên làm

    • Đặt bất biến nghiệp vụ vào constraint của database, không chỉ trong code validate.
    • Chạy EXPLAIN ANALYZE cho mọi query mới đụng tới bảng lớn trước khi merge.
    • Giữ transaction ngắn và không bao giờ gọi HTTP ra ngoài bên trong transaction.
    • Review SQL migration được sinh ra và thử nó trên bản sao dữ liệu cỡ production.

    Dễ sai

    • Load relation trong vòng lặp qua ORM và tạo ra N+1 query.
    • Thêm index hoặc constraint NOT NULL trên bảng đang bận theo cách khóa ghi hàng phút.
    • Dựa vào kiểm tra ở tầng ứng dụng để đảm bảo unique, thứ sẽ bị race khi có request đồng thời.

    Project chốt chặng: Tồn kho chịu được đồng thời

    Mô hình sản phẩm, đơn hàng và tồn kho trên PostgreSQL với Prisma hoặc Drizzle, rồi implement đặt hàng không bao giờ bán vượt tồn kho dưới 100 request đồng thời. Xong khi load test cho thấy không có tồn kho âm, các query nóng dùng index scan trong EXPLAIN, và có một thay đổi schema được ship bằng migration expand and contract.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpCode kiểm tra SELECT xem email đã tồn tại chưa rồi mới INSERT. Vì sao vẫn có thể có user trùng email?
    2. Câu 2/4Trung cấpKhi dùng isolation level Serializable trong PostgreSQL, application cần làm gì?
    3. Câu 3/4Trung cấpQuery WHERE tenant_id = $1 AND created_at > $2 ORDER BY created_at chạy chậm. Index nào phù hợp nhất?
    4. Câu 4/4Trung cấpCần thêm cột status bắt buộc (giá trị tính từ dữ liệu cũ) vào bảng 50 triệu dòng đang nhận ghi liên tục. Cách nào không khóa ghi lâu?
  3. 06Chặng 06, 4 tuầnXác thực và phân quyềnLàm đăng nhập an toàn bằng session hoặc OIDC, hỗ trợ passkey, và áp phân quyền chi tiết trên từng resource.
    Nhìn từ frontend, auth chỉ là lưu token rồi gắn header. Tự làm phía backend dạy mình rằng authentication phần lớn đã được giải bằng chuẩn và thư viện, còn authorization mới là nơi bug thật sự nằm.

    Kiến thức cần nắm

    Session hay token
    Session phía server lưu trong store như Redis cùng cookie HttpOnly, Secure, SameSite thì đơn giản, thu hồi được và rất hợp với web app first-party. JWT access token hợp cho service-to-service và API bên thứ ba, nhưng giữ thời hạn ngắn, kiểm tra iss, aud, exp, alg và đừng lưu trong localStorage.
    OAuth 2.1 và OpenID Connect
    OAuth 2.1 vẫn là IETF draft nhưng gom lại các thực hành hiện hành: authorization code kèm PKCE cho mọi client, bỏ implicit và password grant, so khớp redirect URI chính xác. RFC 9700 là best practice bảo mật đã được công bố; OIDC thêm ID token và discovery để bạn giao việc đăng nhập cho identity provider.
    Passkey và WebAuthn
    Passkey là credential khóa công khai chống phishing, được nền tảng đồng bộ giữa các thiết bị; server lưu public key và xác minh challenge đã ký. Thư viện như SimpleWebAuthn lo phần registration và assertion; cần thiết kế luồng khôi phục tài khoản và giữ một yếu tố dự phòng.
    RBAC, ABAC và phân quyền theo quan hệ
    RBAC gán quyền theo role và đủ dùng cho nhiều app; ABAC thêm thuộc tính như tenant, chủ sở hữu hay khu vực; ReBAC (kiểu Zanzibar, như OpenFGA hoặc SpiceDB) mô hình hóa chia sẻ và phân cấp. Policy engine như Cedar hay OPA giúp đưa luật ra khỏi controller và test riêng được.
    Kiểm tra cấp object và multi-tenancy
    Mọi query phải giới hạn theo tenant hoặc quyền sở hữu của người gọi, không chỉ dựa vào guard ở route. Row-level security của PostgreSQL có thể ép cô lập tenant như một lớp phòng thủ thứ hai.

    Nên làm

    • Dùng thư viện hoặc identity provider đã được kiểm chứng cho luồng đăng nhập thay vì tự viết crypto.
    • Kiểm tra quyền ở mọi lần truy cập object, gần dữ liệu nhất có thể, và mặc định là từ chối.
    • Hash mật khẩu bằng Argon2id hoặc bcrypt và rate limit các endpoint login, reset password.
    • Rotate refresh token và phát hiện việc dùng lại để bắt token bị đánh cắp.

    Dễ sai

    • Tin user ID hay tenant ID gửi trong body thay vì lấy từ principal đã xác thực.
    • Phát JWT sống lâu mà không có cách thu hồi khi user đăng xuất hoặc bị khóa.

    Project chốt chặng: Auth đa tenant

    Thêm đăng nhập OIDC qua một provider hosted hoặc tự host, đăng ký passkey như phương thức thứ hai, và phân quyền theo role kết hợp quyền sở hữu cho service orders. Xong khi test tự động chứng minh user của tenant A nhận 404 hoặc 403 trên mọi resource của tenant B và session bị thu hồi ngừng hoạt động ngay lập tức.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpVì sao JWT sống lâu (ví dụ 30 ngày) là vấn đề khi user đăng xuất hoặc bị khóa tài khoản?
    2. Câu 2/4Trung cấpPKCE bảo vệ authorization code flow khỏi điều gì?
    3. Câu 3/4Trung cấpHandler GET /invoices/:id chạy sau auth guard và gọi invoiceRepo.findById(id). Lỗi gì và sửa thế nào?
    4. Câu 4/4Trung cấpCách nào đúng để lưu mật khẩu người dùng?
  4. 07Chặng 07, 3 tuầnCache, queue và xử lý bất đồng bộDùng Redis cho cache và trạng thái rate limit, đẩy việc chậm sang background job tin cậy, và publish event mà không mất dữ liệu.
    Mình từng nghĩ cache là tăng tốc miễn phí. Phần khó, mà mình học khá chậm, là invalidation và chuyện gì xảy ra khi cache hay queue bị sập.

    Kiến thức cần nắm

    Các pattern cache
    Cache-aside với TTL là mặc định; thêm jitter vào TTL và gộp request (single-flight) để tránh stampede khi một key nóng hết hạn. Chỉ cache thứ bạn chấp nhận được việc cũ đi, và đặt key có prefix version để invalidate cả một dạng dữ liệu cùng lúc.
    Redis và Valkey
    Không chỉ string: hash, sorted set cho bảng xếp hạng và sliding window, stream, và Lua hoặc function cho thao tác nhiều bước nguyên tử. Valkey là bản fork thuộc Linux Foundation ra đời sau khi Redis đổi license, các cloud managed giờ có cả hai nên hãy kiểm tra nền tảng của bạn đang chạy cái nào.
    Background job với BullMQ
    BullMQ cho queue chạy trên Redis với retry, exponential backoff, job delay và lặp lại, rate limit và flow (job cha, job con). NestJS tích hợp qua @nestjs/bullmq; mọi job handler phải idempotent vì at-least-once delivery nghĩa là chắc chắn sẽ có trùng lặp.
    Transactional outbox
    Ghi vào database và publish lên broker là hai hệ thống, nên một bên có thể lỗi sau khi bên kia đã thành công. Ghi event vào bảng outbox trong cùng transaction, rồi relay nó bằng poller hoặc CDC (Debezium, logical replication) sang queue hay broker.
    Khi nào cần broker thật sự
    Queue trên Redis hợp cho xử lý job trong một hệ thống; Kafka, Redpanda, NATS JetStream hay RabbitMQ hợp cho event stream bền vững, fan-out tới nhiều consumer và replay. Chọn dựa trên nhu cầu về thứ tự, thời gian lưu và consumer group, không theo trào lưu.

    Nên làm

    • Làm mọi job và event consumer idempotent bằng key đánh dấu message đã xử lý hoặc unique constraint tự nhiên.
    • Đưa job lỗi vào dead-letter queue kèm đủ context để chạy lại.
    • Đặt TTL cho mọi cache entry và đo hit ratio trước khi thêm cache.
    • Giữ hệ thống đúng khi Redis không khả dụng, dù có chậm đi.

    Dễ sai

    • Publish event ngay sau commit mà không có outbox, rồi âm thầm mất event khi process crash.
    • Dùng cache làm nguồn sự thật cho dữ liệu không được lưu ở đâu khác.

    Project chốt chặng: Pipeline đơn hàng

    Khi có đơn hàng, ghi một dòng outbox trong cùng transaction, relay sang queue BullMQ, và để worker gửi email xác nhận cùng cập nhật bản tóm tắt đơn trong cache. Xong khi kill API hoặc worker giữa chừng mà mỗi đơn vẫn có đúng một email và cache được làm mới trong khoảng thời gian đã đặt.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpMột hot key trong cache hết hạn và 1.000 request cùng lúc đổ vào database. Cách nào giảm vấn đề này?
    2. Câu 2/4Trung cấpVì sao nên dùng transactional outbox thay vì publish event ngay sau khi commit?
    3. Câu 3/4Trung cấpJob BullMQ gửi email chào mừng đôi khi chạy hai lần. Cách sửa đúng?
    4. Câu 4/4Trung cấpRedis dùng làm cache bị sập. Service nên hành xử thế nào?
  5. 08Chặng 08, 3 tuầnTesting cho backend serviceXây bộ test nhanh và đáng tin gồm unit test, integration test với dependency thật và contract test.
    Ở frontend mình dựa nhiều vào component test và mock. Ở backend, mock database cho mình test xanh nhưng query hỏng trên production, nên integration test với PostgreSQL thật thành lớp mình tin nhất.

    Kiến thức cần nắm

    Kim tự tháp test cho service
    Unit test cho logic domain thuần, integration test cho repository, controller và queue handler, và một số ít luồng end-to-end. Nhiều team giờ chuộng hình testing trophy cho service, nơi integration test gánh phần lớn độ tin cậy.
    Integration test với Testcontainers
    Testcontainers khởi động PostgreSQL, Redis hay Kafka thật trong Docker cho mỗi lần chạy test, nên bạn test được SQL, constraint và migration thật. Chạy migration một lần, cô lập test bằng transaction hoặc truncate, và reuse container ở local để vòng lặp nhanh.
    Tiện ích test của NestJS
    Test.createTestingModule kèm overrideProvider giúp thay service bên ngoài, còn supertest gọi tầng HTTP của một app instance thật. Vitest hay Jest đều dùng được; Vitest kèm SWC khá phổ biến trong dự án TypeScript vì nhanh.
    Contract testing
    Consumer-driven contract với Pact xác minh provider vẫn đáp ứng đúng những gì consumer thật sự dùng, không cần một môi trường chung đầy đủ. Với team OpenAPI-first, kiểm tra theo schema và diff breaking change trong CI bắt được nhiều lỗi tương tự.
    Test bất đồng bộ và thời gian
    Dùng fake timer hoặc inject clock cho logic hết hạn, test queue handler bằng cách gọi trực tiếp cộng thêm một luồng integration qua queue thật. Test retry và idempotency một cách tường minh bằng cách gửi cùng một message hai lần.

    Nên làm

    • Test trên cùng engine và phiên bản database với production.
    • Tạo dữ liệu test bằng các factory function nhỏ thay vì fixture dùng chung cồng kềnh.
    • Chạy toàn bộ integration test ở mọi pull request và giữ thời gian dưới vài phút.
    • Thêm regression test cho mọi bug production trước khi sửa.

    Dễ sai

    • Mock ORM kỹ tới mức test đang kiểm tra mock chứ không phải query.
    • Test phụ thuộc thứ tự chạy hoặc dữ liệu dùng chung có thể thay đổi, rồi fail ngẫu nhiên trên CI.

    Project chốt chặng: Bộ test đáng tin

    Thêm unit test cho logic tính giá, integration test dùng Testcontainers cho repository và API orders, và một bước kiểm tra Pact hoặc diff OpenAPI giữa service với client được sinh ra. Xong khi bộ test chạy trên CI dưới năm phút, bắt được một breaking change cố ý chèn vào, và tái hiện được race bán vượt tồn kho ở stage 05.

    • VitestVitest
    • Testcontainers
    • Pact

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Trung cấpVì sao mock ORM quá kỹ trong test repository là cách làm yếu?
    2. Câu 2/4Trung cấpVì sao chạy integration test với PostgreSQL thật qua Testcontainers tốt hơn SQLite in-memory?
    3. Câu 3/4Trung cấpLàm sao test rằng consumer xử lý event OrderPaid là idempotent?
    4. Câu 4/4Trung cấpCần test logic token hết hạn sau 15 phút. Cách nào đúng?

Nâng cao09–10

  1. 09Chặng 09, 4 tuầnObservability và bảo mật APILàm cho service debug được trên production bằng log, trace và metric, và gia cố nó trước OWASP API Top 10.
    Mình gộp hai phần này vì cả hai đều nói về chuyện xảy ra sau khi deploy, lúc bạn không ngồi nhìn. Lần đầu thử debug lỗi production từ mấy dòng console.log không có cấu trúc, mình hiểu vì sao cả hai đều quan trọng.

    Kiến thức cần nắm

    Structured logging
    Log dạng JSON bằng Pino (nestjs-pino để tích hợp), gắn request ID hoặc trace ID trên mọi dòng, dùng level nhất quán. Không bao giờ log secret, token hay dữ liệu cá nhân; redact theo path ngay trong cấu hình logger.
    Trace và metric với OpenTelemetry
    OpenTelemetry là chuẩn trung lập với vendor cho trace, metric và log; Node SDK kèm auto-instrumentation phủ HTTP, PostgreSQL, Redis và nhiều thứ khác. Export qua OTLP tới Collector, rồi tới backend bất kỳ như Grafana, Jaeger, Honeycomb hay Datadog, và theo dõi RED metric (rate, errors, duration) cho từng route.
    SLO và alerting
    Đặt service level objective trên độ trễ và tỷ lệ lỗi mà người dùng cảm nhận, alert theo tốc độ đốt error budget thay vì mỗi lần CPU tăng vọt. Dashboard trả lời câu hỏi đã biết; trace với attribute high-cardinality trả lời câu hỏi chưa biết.
    OWASP API Security Top 10
    Bản 2023 vẫn là danh sách API hiện hành: broken object level authorization (BOLA), broken authentication, broken object property level authorization, unrestricted resource consumption, broken function level authorization, unrestricted access to sensitive business flows, SSRF, security misconfiguration, improper inventory management và unsafe consumption of APIs. BOLA đứng đầu là có lý do, nên hãy test nó trên mọi endpoint.
    Gia cố service
    Giới hạn kích thước body, timeout và rate limit, chặn SSRF bằng allowlist host gọi ra ngoài, đặt security header bằng Helmet. Scan dependency và container image trong CI, pin lockfile, và coi tấn công supply chain trên npm là rủi ro thật.

    Nên làm

    • Truyền một trace ID duy nhất từ edge qua các lời gọi HTTP, queue và log.
    • Redact field nhạy cảm trong logger, không phải ở từng chỗ gọi log.
    • Với mỗi endpoint mới, thêm một test phân quyền thử truy cập resource ID của user khác.
    • Chạy scan dependency và image ở mọi build và fail khi có lỗ hổng critical.

    Dễ sai

    • Log nguyên request body có mật khẩu hoặc token vào nền tảng log dùng chung.
    • Để field nội bộ như role hay isAdmin bị mass assignment qua DTO.

    Project chốt chặng: Telemetry sẵn sàng production

    Instrument service bằng OpenTelemetry và Pino, export tới Collector chạy local cùng Grafana hoặc Jaeger, và định nghĩa một SLO độ trễ kèm alert. Xong khi một request đặt hàng hiện thành một trace xuyên suốt API, database và queue worker, và một checklist bảo mật map theo OWASP API Top 10 đều pass bằng test.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Nâng caoBOLA (broken object level authorization), mục đứng đầu OWASP API Security Top 10, là gì?
    2. Câu 2/4Nâng caoVì sao nên redact field nhạy cảm trong cấu hình logger (ví dụ redact paths của Pino) thay vì ở từng chỗ gọi log?
    3. Câu 3/4Nâng caoEndpoint tạo link preview fetch một URL do người dùng gửi. Attacker gửi http://169.254.169.254/latest/meta-data/. Cách sửa đúng?
    4. Câu 4/4Nâng caoTeam bị mệt vì alert liên tục. Alert nào nên giữ cho dịch vụ checkout?
  2. 10Chặng 10, 6 tuầnScaling, hệ phân tán và deployScale service theo chiều ngang, lập luận được về consistency và lỗi, và deploy an toàn bằng container cùng health check.
    Đây là phần mình vẫn đang học nhiều nhất, chủ yếu qua đọc và làm side project chứ chưa vận hành hệ thống lớn. Điều mình muốn nói với bản thân ngày trước: phần lớn vấn đề scaling được giải bằng một query tốt hơn, một index hay một queue, rất lâu trước khi cần tới microservices.

    Kiến thức cần nắm

    Consistency và mô hình lỗi
    Hiểu linearizability, read-your-writes và eventual consistency, và rằng replica bị trễ nên đọc ngay sau khi ghi có thể ra dữ liệu cũ. Timeout, retry với exponential backoff và jitter, cùng circuit breaker giữ cho một dependency chậm không kéo sập cả hệ thống.
    Rate limiting và load shedding
    Token bucket hoặc sliding window counter trên Redis bảo vệ tài nguyên chung theo user, API key hay tenant, và trả 429 kèm Retry-After. Chủ động từ chối bớt tải sớm khi queue hay độ trễ tăng thay vì để mọi thứ cùng timeout.
    Kiến trúc event-driven
    Event giúp tách rời các service, nhưng bạn đổi transaction đơn giản lấy saga, consumer idempotent và eventual consistency. Bắt đầu bằng modular monolith với ranh giới module rõ ràng; chỉ tách service khi ranh giới team hoặc nhu cầu scale đòi hỏi.
    Container và pipeline deploy
    Docker multi-stage build với base Node gọn, user không phải root và chỉ production dependency; chạy migration như một bước riêng trước khi rollout. Nền tảng trải từ Kubernetes tới dịch vụ managed như Cloud Run, Fly.io hay Railway; chọn cái đơn giản nhất đáp ứng được nhu cầu.
    Health, rollout và cấu hình
    Tách riêng endpoint liveness và readiness, xử lý SIGTERM với khoảng drain, và rollout theo rolling, blue-green hoặc canary kèm feature flag. Theo twelve-factor cho config qua biến môi trường và secret manager, không bao giờ nhúng vào image.

    Nên làm

    • Đo trước bằng load test (k6 hoặc autocannon) và sửa query chậm nhất trước khi thêm hạ tầng.
    • Giữ service stateless để instance nào cũng phục vụ được mọi request.
    • Đặt timeout cho mọi lời gọi ra ngoài, kể cả client database và Redis.
    • Làm deploy có thể đảo ngược: migration tương thích ngược và rollback bằng một lệnh.

    Dễ sai

    • Tách microservices quá sớm và kết thúc với một distributed monolith phải deploy cùng nhau.
    • Retry ngay lập tức không có backoff, biến một sự cố nhỏ thành cơn bão retry.
    • Dùng chung một health endpoint cho cả liveness và readiness, khiến container bị restart mỗi khi dependency chập chờn.

    Project chốt chặng: Ship rồi thử phá

    Container hóa service, deploy từ hai instance trở lên sau load balancer trên một nền tảng managed hoặc Kubernetes local, và thêm rate limit theo tenant. Xong khi test k6 cho thấy p95 latency ổn định ở mức tải mục tiêu, kill một instance không làm rớt request nào, và một bản release lỗi rollback được bằng một lệnh.

    Kiểm tra nhanh

    Đã trả lời 0/4 · đúng 0

    Trả lời đúng ít nhất 3/4 câu để đánh dấu đã qua chặng này.

    1. Câu 1/4Nâng caoVì sao không nên để liveness probe kiểm tra kết nối database?
    2. Câu 2/4Nâng caoVì sao retry cần exponential backoff kèm jitter?
    3. Câu 3/4Nâng caoMột API key vượt giới hạn request mỗi phút. Response đúng là gì?
    4. Câu 4/4Nâng caoUser cập nhật profile (ghi vào primary), trang kế tiếp đọc từ read replica và vẫn hiện dữ liệu cũ. Cách sửa đúng?

Những gì đang quan trọng lúc này và hướng đi trong vài năm tới.

Hiện tại

  • TypeScript ở khắp nơi, runtime nhẹ hơn

    Node 24 LTS chạy TypeScript qua type stripping, Bun và Deno đã trưởng thành, và ORM như Prisma 7 bỏ engine native để chuyển hẳn sang TypeScript. Kết quả là tooling gọn hơn, nhưng framework dựa vào decorator metadata như NestJS vẫn cần một bước compile thật.

  • PostgreSQL là database mặc định

    PostgreSQL 18 thêm asynchronous I/O và uuidv7() có sẵn, còn các extension (pgvector, PostGIS, full text) giúp một database phủ được những workload trước đây cần nhiều hệ thống. Hiểu sâu Postgres giờ là kỹ năng cốt lõi của backend, không còn là chuyên môn riêng của DBA.

  • Hợp đồng và chuẩn thay cho code dán tạm

    OpenAPI 3.2, RFC 9457 Problem Details, thực hành OAuth 2.1 cùng RFC 9700 và OpenTelemetry tạo ra ngôn ngữ chung giữa các team và công cụ. Sinh client, tài liệu và test từ hợp đồng giờ là điều được kỳ vọng chứ không còn là điểm cộng.

  • Phân quyền và lạm dụng API là rủi ro chính

    OWASP API Top 10 vẫn xếp broken object level authorization đứng đầu, và việc bot lạm dụng luồng nghiệp vụ tiếp tục tăng. Phân quyền chi tiết, rate limiting và passkey đang chuyển từ chủ đề nâng cao thành mức cơ bản.

Sắp tới

  • API được AI agent sử dụng

    Ngày càng nhiều traffic sẽ đến từ LLM agent gọi API qua các giao thức tool như MCP, khiến schema rõ ràng, thông báo lỗi tốt, idempotency và token có phạm vi hẹp càng quan trọng. Chuẩn bị bằng cách viết mô tả OpenAPI chính xác và thiết kế các thao tác an toàn, giới hạn theo quyền.

  • Durable execution và workflow engine

    Các công cụ như Temporal và những workflow engine bền vững khác lo retry, timer và tiến trình chạy dài mà trước đây team phải tự ghép từ queue và cron. Hãy nắm outbox, saga và idempotency trước để các engine này trở thành bước tiếp theo tự nhiên.

  • Phân quyền dạng policy-as-code

    Engine dựa trên quan hệ và policy như OpenFGA, SpiceDB, Cedar và OPA đang phổ biến dần khi app thêm tính năng chia sẻ, multi-tenancy và quyền cho agent. Tập tách luật phân quyền khỏi handler và test chúng như dữ liệu.

  • Observability mặc định và platform engineering

    OpenTelemetry đang thành lớp instrumentation mặc định, và internal developer platform che bớt phần lớn Kubernetes. Backend engineer sẽ được kỳ vọng tự chịu trách nhiệm SLO và hành vi production của service mình, nên hãy quen với việc đọc trace và định nghĩa alert.

Đi hết roadmap, bạn sẽ

  • Bạn thiết kế được REST API với OpenAPI, lỗi nhất quán, thao tác ghi idempotent và cursor pagination, và giải thích được khi nào GraphQL hay tRPC hợp hơn.
  • Bạn tổ chức được service NestJS với module gọn, guard, pipe, interceptor và config được validate.
  • Bạn mô hình hóa được schema PostgreSQL, đọc query plan, chọn isolation level và ship migration không downtime qua Prisma hoặc Drizzle.
  • Bạn làm được đăng nhập bằng session hoặc OIDC kèm passkey, và áp phân quyền cấp object có nhận biết tenant.
  • Bạn đẩy được việc chậm sang job BullMQ idempotent và publish event tin cậy bằng transactional outbox.
  • Bạn test được service bằng Testcontainers và contract test, và trace một request xuyên qua API, database và worker bằng OpenTelemetry.
  • Bạn container hóa, deploy và scale được một service stateless với health check, rate limiting và rollback an toàn.

Bài viết liên quan

Tất cả bài viết

Câu hỏi phỏng vấn Backend Engineer

Xem cả 40 câu

Các role khác

Roadmap

Frontend Engineer

Role chính của mình

Xây giao diện web nhanh, dễ tiếp cận và bền vững khi sản phẩm lẫn team cùng lớn lên

10 chặng, 51 chủ đề, ~34 tuần

Xây app iOS và Android mượt, chạy được khi mất mạng và phát hành an toàn

10 chặng, 50 chủ đề, ~34 tuần

Tìm rủi ro trước người dùng và biến chất lượng thành thứ cả team đo được

10 chặng, 50 chủ đề, ~36 tuần

Xây hạ tầng, pipeline và nền tảng để team ship nhanh, chạy ổn định, an toàn

10 chặng, 56 chủ đề, ~42 tuần

Xây sản phẩm trên nền LLM: từ gọi API, RAG, agent đến eval và vận hành production

10 chặng, 58 chủ đề, ~31 tuần