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

Luyện trả lời theo đúng trình độ bạn đang ứng tuyển. Mỗi câu có đáp án ngắn gọn và điều người phỏng vấn thật sự muốn nghe.

  • Junior 0–2 năm: nền tảng, công cụ hằng ngày, làm việc an toàn trong team.
  • Middle 2–5 năm: tự chịu trách nhiệm một tính năng, hiểu đánh đổi.
  • Senior 5+ năm: thiết kế hệ thống, dẫn dắt kỹ thuật, quyết định dài hạn.
  1. JuniorAIAI viết code gọi một hàm của thư viện mà bạn chưa từng thấy. Bạn kiểm tra nó có thật không bằng cách nào?

    Người phỏng vấn muốn biết bạn có coi code do AI viết là bản nháp cần kiểm chứng hay không, và bạn có biết những cách kiểm chứng rẻ, nhanh, đáng tin hay chỉ hỏi lại AI.

    Cách đáng tin nhất là dùng nguồn mà AI không thể bịa. Bạn tìm tên hàm trong code hoặc file kiểu của thư viện đang cài, ví dụ tìm trong node_modules. Nếu tìm không ra thì hàm không có ở phiên bản đang dùng. Hỏi lại AI "hàm này có thật không" là cách kém tin cậy, vì nó có thể trả lời chắc chắn theo đúng cách đã bịa lúc đầu.

    Có ba lớp kiểm chứng, từ rẻ đến kỹ:

    1. Chạy thử. Bắt được lỗi trên đường chương trình đi qua, nhưng chỉ thấy lỗi từng cái một, và lỗi nằm trong nhánh ít chạy sẽ nằm im tới khi có người dùng thật đụng tới.
    2. Trình biên dịch hoặc linter. Với TypeScript, tsc --noEmit đọc cả file và báo mọi lời gọi sai hình dạng trong một lượt. Một lỗi ở đầu chuỗi có thể kéo theo các lỗi hệ quả, nên sửa lỗi đầu rồi chạy lại.
    3. Test. Bắt được hành vi mà bạn đã nghĩ ra để kiểm tra, ví dụ "sửa tin nhắn của người này không đụng tới sổ của người kia".

    Ý quan trọng để nêu: cả ba lớp đều không bắt được code đúng hình dạng nhưng sai ý nghĩa. Một hàm có thật nhưng dùng sai, hay tìm trong dữ liệu chung thay vì dữ liệu của riêng từng người, vẫn qua hết. Vì vậy cuối cùng cần đọc tài liệu chính thức cho những điều như "mã này có duy nhất trong phạm vi nào" và tự viết test cho các tình huống quan trọng.

    Điểm cộng: kể rằng bạn coi mỗi hàm lạ AI đưa ra là một câu hỏi cần trả lời chứ không phải câu trả lời, và nhắc rằng tên hàm bịa thường mượn từ thư viện tương tự (như hàm của Telegraf trong grammY).

    Mở trang riêng của câu hỏi
  2. JuniorAILàm sao biết một unit test do AI viết thật sự có tác dụng?

    Người phỏng vấn muốn biết bạn có coi test như một thứ cần được kiểm tra, hay chỉ thấy xanh là yên tâm. Câu trả lời tốt cho thấy bạn hiểu test có giá trị khi nó đỏ lúc code sai.

    Ý chính: một test xanh với code đúng chưa chứng minh được gì. Cách kiểm tra là làm hỏng code có chủ đích. Bạn sửa một chỗ nhỏ, như đảo một điều kiện, bỏ một dòng, đổi một hằng số, rồi chạy lại test. Nếu test vẫn xanh thì nó chưa bảo vệ chỗ đó. Kỹ thuật này gọi là mutation testing, và có công cụ làm tự động như Stryker cho JavaScript.

    Có thể nêu thêm bốn thói quen:

    1. Nói rõ điều cần bảo vệ khi nhờ AI viết test, ví dụ "hai người dùng không dùng chung dữ liệu", thay vì chỉ nói "viết test cho file này".
    2. Đừng tin độ phủ (coverage) một mình. Độ phủ chỉ cho biết dòng code đã chạy qua, không cho biết kết quả có được kiểm tra hay không. Hai bộ test có thể cùng độ phủ 100% mà một bộ bắt được ít lỗi hơn hẳn.
    3. Đọc từng câu assert. Kiểm tra kiểu "có trả lời" hay "chứa chữ này" rất lỏng. So sánh với kết quả đầy đủ chặt hơn nhiều.
    4. Thử đúng các lỗi đã từng xảy ra. Mỗi lỗi từng gặp nên có ít nhất một test sẽ đỏ nếu nó quay lại.

    Điểm cộng: kể một ví dụ cụ thể, chẳng hạn một bot có test cho tính năng ghi chi tiêu nhưng chỉ thử với một người dùng, nên khi code vô tình dùng chung một danh sách cho mọi người, test vẫn xanh. Chỉ khi thêm test với hai người dùng thì lỗi mới lộ ra.

    Mở trang riêng của câu hỏi
  3. JuniorAIRAG là gì, và khi nào nên dùng RAG thay vì gửi cả tài liệu vào prompt?

    Người phỏng vấn muốn biết bạn hiểu RAG như một quy trình có các bước, chứ không chỉ như một cái tên thời thượng, và bạn có biết cân nhắc đánh đổi hay không.

    RAG, viết tắt của retrieval-augmented generation, gồm ba bước. Một là tách tài liệu thành các đoạn nhỏ, làm một lần từ trước. Hai là khi có câu hỏi, tìm vài đoạn liên quan nhất, bằng đếm từ khóa hoặc bằng cách so nghĩa. Ba là ghép các đoạn đó vào prompt rồi mới gọi mô hình, kèm yêu cầu chỉ trả lời dựa trên các đoạn đó.

    Nên dùng RAG khi:

    1. Tài liệu đủ dài để gửi cả tài liệu là tốn. Mọi thứ trong prompt được gửi lại ở mỗi lần gọi, nên chi phí và độ trễ tăng theo độ dài tài liệu, còn RAG chỉ gửi vài đoạn.
    2. Tài liệu thay đổi thường xuyên. Sửa tài liệu và cập nhật chỉ mục, không phải viết lại prompt.
    3. Bạn cần biết câu trả lời dựa vào đâu. Mỗi đoạn có tên, nên có thể bắt mô hình trích nguồn và kiểm tra lại.

    Không nên dùng RAG khi tài liệu chỉ vài trang. Gửi cả tài liệu đơn giản hơn và bớt một chỗ có thể sai.

    Ý quan trọng để nêu: RAG thêm một chỗ để lỗi xảy ra là bước tìm kiếm. Nếu bước đó không đưa đúng đoạn, mô hình có thể trả lời thiếu hoặc đoán, trong khi câu trả lời vẫn nghe rất trôi chảy. Vì vậy khi RAG trả lời sai, việc đầu tiên là xem các đoạn nào đã được gửi đi, chứ không phải sửa prompt ngay.

    Điểm cộng: kể một ví dụ cụ thể, như khách hỏi "tiền về bao lâu" trong khi tài liệu viết "thời hạn hoàn tiền". Tìm theo từ khóa bỏ sót câu này vì hai cách nói không có chữ nào chung, và đó là lý do người ta chuyển sang tìm theo nghĩa bằng embedding.

    Mở trang riêng của câu hỏi
  4. JuniorAI AgentAI agent khác chatbot và workflow ở điểm nào? Khi nào không nên dùng agent?

    Câu trả lời tốt phân biệt ba thứ theo một tiêu chí: ai quyết định bước tiếp theo.

    • Chatbot theo luật: luật viết tay quyết định (ví dụ dò từ khóa). Rẻ, nhưng không hiểu câu; "đơn của em giao chưa" có thể bị hiểu thành hỏi phí ship vì có chữ "giao".
    • Workflow: lập trình viên viết sẵn đường đi trong code; LLM chỉ làm từng bước nhỏ như phân loại hay viết câu trả lời. Dễ đoán, dễ kiểm thử, rẻ, nhưng chỉ đi được những đường đã viết.
    • Agent: LLM tự chọn bước tiếp theo trong một vòng lặp: gọi công cụ, đọc kết quả, quyết định gọi tiếp hay trả lời. Xử lý được yêu cầu nhiều bước, có điều kiện, như "nếu đơn chưa giao thì thêm một món".

    Không nên dùng agent khi:

    • các loại yêu cầu biết trước và mỗi loại có cách xử lý rõ ràng (workflow đủ, nhanh và rẻ hơn);
    • một lần gọi LLM kèm thông tin phù hợp đã giải quyết được;
    • hành động có rủi ro cao (tiền, xóa dữ liệu) mà chưa có người duyệt.

    Điểm cộng: nhắc tới giới hạn số bước của vòng lặp, chi phí mỗi bước là một lần gọi mô hình, và thiết kế kết hợp, tức workflow ở cửa vào, agent chỉ cho yêu cầu phức tạp.

    Mở trang riêng của câu hỏi
  5. JuniorAI AgentLLM hallucination là gì? Khi xây chatbot chăm sóc khách hàng, bạn giảm nó bằng cách nào?

    Hallucination là khi mô hình ngôn ngữ sinh ra thông tin nghe hợp lý nhưng sai, hoặc không có trong dữ liệu được cung cấp. Ví dụ, khách hỏi "đơn 1041 giao chưa" mà request gửi cho mô hình không hề có dữ liệu đơn hàng, mô hình vẫn có thể trả lời "đơn của bạn đã giao rồi ạ".

    Ý quan trọng cần nói được là mô hình chỉ biết những gì nằm trong request cộng với kiến thức chung nó đã học. Nó không tự kết nối với database của bạn. Vì vậy, với dữ liệu thay đổi liên tục như trạng thái đơn, viết prompt khéo hơn không phải là cách sửa.

    Các cách giảm, từ dễ đến chắc chắn:

    • Đưa thông tin cần thiết vào prompt (giờ mở cửa, bảng giá) và dặn "chỉ dùng thông tin này".
    • Cho phép mô hình nói "chưa biết", kèm câu thay thế cụ thể. Tài liệu của Anthropic coi đây là kỹ thuật cơ bản đầu tiên.
    • Với dữ liệu động, để code tra dữ liệu thật rồi đưa kết quả cho mô hình, tức là dùng tool calling hoặc RAG.
    • Thêm lớp kiểm tra bằng code cho những câu trả lời rủi ro, và có test cho lớp đó.

    Điểm cộng: nói rõ các kỹ thuật này giảm chứ không loại bỏ hoàn toàn hallucination, nên việc có rủi ro cao (tiền, hứa hẹn với khách) vẫn cần người duyệt.

    Mở trang riêng của câu hỏi
  6. JuniorAI AgentTool calling (function calling) là gì? Khi dùng tool calling, mô hình có tự chạy hàm của bạn không?

    Tool calling là cách cho LLM dùng dữ liệu và hành động bên ngoài nó. Ý quan trọng nhất cần nói là mô hình không tự chạy hàm nào cả: nó chỉ viết ra một yêu cầu, còn code của bạn quyết định có chạy hay không.

    Một lần gọi công cụ đi qua bốn bước:

    1. Ứng dụng gửi request kèm danh sách công cụ, mỗi công cụ có tên, mô tả (khi nào nên dùng) và JSON Schema cho đầu vào.
    2. Nếu cần, mô hình trả về khối tool_use (tên công cụ và đối số) với stop_reason là tool_use, thay vì câu trả lời.
    3. Ứng dụng chạy hàm thật, ví dụ đọc đơn 1041 trong database, rồi gửi kết quả lại trong khối tool_result có cùng id.
    4. Mô hình đọc kết quả và viết câu trả lời cuối.

    Nên nói thêm vài ý để cho thấy bạn đã làm thật:

    • Đầu vào do mô hình viết là dữ liệu không đáng tin, phải kiểm tra như dữ liệu người dùng gửi lên (định dạng, quyền truy cập). Schema chỉ bảo đảm kiểu dữ liệu, không bảo đảm giá trị hợp lý.
    • Lỗi của công cụ nên trả về cho mô hình (ví dụ is_error: true, "không có đơn 1099") để nó hỏi lại khách, thay vì làm cả ứng dụng dừng.
    • Mỗi lần dùng công cụ tốn thêm một lượt gọi API, và định nghĩa công cụ được tính vào token đầu vào ở mọi request.
    • Khi câu hỏi cần nhiều bước (tìm mã đơn theo số điện thoại rồi mới xem trạng thái), ứng dụng phải lặp cho tới khi mô hình trả lời bằng chữ. Đó là vòng lặp agent.
    Mở trang riêng của câu hỏi
  7. JuniorAI cho developerBạn chọn công cụ AI hỗ trợ lập trình (Copilot, Cursor, Claude Code…) dựa trên tiêu chí nào?

    Câu trả lời tốt không xếp hạng công cụ, mà cho thấy bạn hiểu chúng khác nhau ở đâu và bạn kiểm soát kết quả thế nào.

    Tiêu chí đầu tiên là mức độ giao việc. Gợi ý khi gõ (Copilot, Tab của Cursor) giữ bạn là người viết từng dòng. Agent trong editor sửa nhiều file và để bạn duyệt từng thay đổi. Agent tự chạy lệnh như Claude Code tự chạy test, đọc lỗi, sửa tiếp. Giao càng nhiều thì càng phải review kỹ.

    Tiêu chí thứ hai là quy định của công ty: công cụ nào được phép, code có được gửi ra ngoài không, có chính sách dữ liệu nào không. Ở công ty, đây thường là tiêu chí quyết định.

    Tiêu chí thứ ba là chi phí và gói miễn phí, vì giá thay đổi liên tục, nên luôn xem trang giá chính thức.

    Phần quan trọng nhất là cách bạn kiểm tra kết quả, dù dùng công cụ nào:

    • viết spec ngắn và test trước khi giao việc;
    • dặn rõ "đừng sửa test";
    • tự chạy test với điều kiện giống môi trường thật (ví dụ múi giờ UTC);
    • thử thêm vài trường hợp ngoài test, và giải thích được từng đoạn code trước khi merge.

    Điểm cộng: kể một lỗi cụ thể mà test hoặc việc tự thử thêm đã bắt được.

    Mở trang riêng của câu hỏi
  8. JuniorAI cho developerBạn debug một lỗi khó như thế nào? Bạn dùng AI ở bước nào?

    Người phỏng vấn muốn nghe một quy trình, không phải một công cụ. Câu trả lời tốt có các bước rõ ràng và cho thấy bạn kiểm chứng trước khi tin.

    Một quy trình bạn có thể trình bày:

    1. Tái hiện lỗi bằng cách nhỏ nhất có thể: một lệnh, một script, một test. Chưa tái hiện được thì chưa nên sửa, vì bạn không có cách biết mình đã sửa xong.
    2. Ghi rõ kết quả thật và kết quả mong muốn. "Tổng sai" không đủ; "tổng ra 67.000đ, lẽ ra 35.000đ" thì đủ.
    3. Lần theo dữ liệu, từ chỗ nó vào tới chỗ nó sai: tạo ở đâu, đi qua hàm nào, ai dùng chung. Đọc stack trace nếu có, đặt breakpoint thay vì rải console.log.
    4. Loại trừ có bằng chứng: phần nào đã có test xanh, phần nào đã kiểm tra bằng tay.
    5. Sửa, rồi chạy lại cách tái hiện và toàn bộ test. Tìm mọi nơi gọi hàm mà bạn vừa đổi.

    Về AI, nên nói rõ bạn dùng nó ở đâu và không dùng ở đâu. AI giúp nhiều ở bước 3: đọc code lạ, gợi ý chỗ đáng nghi, giải thích một thông báo lỗi. Nhưng AI chỉ thấy những gì bạn đưa cho nó, nên bạn đưa cả cách tái hiện, kết quả thật, kết quả mong muốn, những file mà dữ liệu đi qua và những gì đã loại trừ. Bạn cũng yêu cầu nó chỉ ra file và dòng trước khi sửa, để tự kiểm tra nguyên nhân.

    Điểm cộng: kể một lỗi cụ thể, ví dụ một lỗi chỉ xuất hiện khi có hai người dùng cùng lúc, và nói bạn đã làm gì để nó không quay lại (một test tái hiện đúng lỗi đó).

    Mở trang riêng của câu hỏi
  9. JuniorAI cho developerBạn dùng AI review code thế nào, và kiểm tra nhận xét của nó ra sao?

    Người phỏng vấn muốn biết bạn dùng AI như một người review thứ hai có kiểm soát, không phải một người phán quyết, và bạn có giữ được khả năng tự đọc code hay không.

    Mình tự review trước, rồi mới nhờ AI. Nếu xem danh sách của AI trước, mình bị neo vào đó và chỉ đánh giá những gì nó nêu, thay vì tự tìm. Khi tự review, mình dùng khung quen thuộc: thiết kế, chức năng và trường hợp biên, độ phức tạp, test, tên gọi (Google có tài liệu hướng dẫn về những điều cần nhìn khi review).

    Khi nhờ AI, mình đòi mỗi nhận xét kèm dòng liên quan, một đầu vào cụ thể gây lỗi, và mức độ, để có thứ mà chạy thử. Rồi mình kiểm từng nhận xét: viết một phép thử nhỏ, hoặc chạy đầu vào đó, xem lỗi có thật không. Bước này quan trọng vì AI có thể nói sai mà rất tự tin, ví dụ khẳng định một hàm sẽ ném lỗi trong khi nó lặng lẽ trả về giá trị sai. Lỗi phụ thuộc môi trường như múi giờ cần được thử ở nhiều cấu hình.

    Cuối cùng mình ghi lại mỗi lần review: mình bắt được gì trước, chỉ AI bắt được gì, AI nói sai gì. Cột "chỉ AI bắt được" cho biết mình đang yếu ở loại lỗi nào.

    Điểm cộng: nhắc rằng AI thiếu bối cảnh cho câu hỏi thiết kế (có nên thêm tính năng này lúc này không), nên coi câu trả lời về thiết kế là ý kiến, và không dán khóa API hay dữ liệu khách hàng vào prompt.

    Mở trang riêng của câu hỏi
  10. JuniorAI cho developerBạn dùng công cụ AI khi lập trình như thế nào? Làm sao biết code AI viết là đúng?

    Người phỏng vấn không hỏi bạn có dùng AI không; họ muốn biết bạn có kiểm soát được code mình nộp hay không. Một câu trả lời tốt mô tả một quy trình cụ thể:

    1. Định nghĩa "đúng" trước khi nhờ AI. Viết yêu cầu ngắn kèm vài ví dụ đầu vào và kết quả mong muốn, gồm cả trường hợp lỗi. Ví dụ: tin nhắn "cà phê 35k" phải ra 35.000đ, tin nhắn không có số tiền phải bị từ chối.
    2. Biến ví dụ thành test, để AI viết code cho tới khi test qua, và chính mình chạy lại test.
    3. Đọc và giải thích được từng đoạn code trước khi commit. Đoạn nào không giải thích được thì hỏi lại hoặc viết lại.
    4. Thử thêm các trường hợp ngoài ví dụ. Test chỉ bảo vệ những gì mình đã nghĩ tới; ví dụ "35 nghìn" có thể bị đọc thành 35 đồng mà test vẫn xanh.
    5. Kiểm chứng API và thư viện với tài liệu chính thống, vì AI có thể gọi hàm không tồn tại hoặc dùng cú pháp cũ.
    6. Không dán bí mật (API key, dữ liệu khách hàng, code công ty khi chưa được phép) vào công cụ AI.

    Điểm cộng: kể một lần cụ thể bạn phát hiện code AI sai và cách bạn phát hiện ra.

    Mở trang riêng của câu hỏi
  11. JuniorAI cho developerKhi nhờ AI viết hoặc sửa code, bạn viết prompt thế nào để kết quả dùng được?

    Người phỏng vấn không cần một "câu thần chú". Họ muốn biết bạn có giữ quyền kiểm soát hay chỉ dán yêu cầu rồi nhận code. Một câu trả lời tốt nêu ba thứ mà prompt nào cũng nên nói rõ:

    1. Việc cần làm, kèm ngữ cảnh đủ dùng: file nào, spec nào, điều kiện chạy (ví dụ test chạy với TZ=UTC).
    2. Việc không được làm: "chưa viết code, hãy hỏi lại chỗ chưa rõ", "không sửa test", "không thêm thư viện", "chỉ sửa đúng vấn đề số 1". Thiếu ràng buộc, AI dễ chọn con đường ngắn nhất, ví dụ sửa test cho xanh.
    3. Cách kiểm tra kết quả: "chạy test và dán kết quả", "test mới phải đỏ trên code hiện tại", "với mỗi khẳng định, đưa một lệnh để mình tự kiểm chứng".

    Nên kể một ví dụ cụ thể. Chẳng hạn: trước khi sửa một lệnh trong app, bạn nhờ AI liệt kê các đầu vào dễ sai, tự chạy thử chúng và phát hiện tin nhắn "/tong tháng 10" bị ghi thành một khoản chi 10 đồng. Sau đó bạn viết test đỏ trước, rồi mới nhờ AI sửa code mà không được đụng vào test.

    Điểm cộng: nói rằng bước kiểm tra là phần không giao cho AI. Lời giải thích nghe hợp lý chưa chắc đã đúng, nên bạn chạy lệnh, đo, đọc diff, rồi mới chấp nhận.

    Mở trang riêng của câu hỏi
  12. JuniorAI cho developerKhi phải dùng một thư viện hoặc nhận một codebase bạn chưa từng thấy, bạn tìm hiểu nó thế nào?

    Người phỏng vấn muốn biết bạn có phương pháp hay chỉ thử sai cho tới khi chạy. Một câu trả lời tốt đi theo thứ tự từ rộng tới hẹp:

    1. Xác định phiên bản và điểm vào. Xem package.json đang dùng bản nào, đọc README và file điểm vào (ví dụ index/mod). Với codebase, chạy được dự án và test trước khi đọc sâu.
    2. Đi theo một luồng cụ thể thay vì đọc hết. Chọn đúng việc mình cần làm (ví dụ "nhận một tin nhắn và trả lời") rồi lần theo từ hàm công khai vào trong.
    3. Liệt kê giả định và kiểm chứng từng cái. Ví dụ với một thư viện Telegram: trường date tính bằng giây hay mili giây, tin nhắn được sửa có đi qua handler không, handler ném lỗi thì chuyện gì xảy ra. Mỗi câu trả lời phải chỉ ra được file và dòng trong mã nguồn hoặc tài liệu chính thức.
    4. Viết một script nhỏ chạy được để thấy hành vi thật, tốt nhất là không cần mạng hay dịch vụ ngoài.

    Nếu bạn dùng AI, hãy nói rõ cách dùng: cho nó đọc mã nguồn của đúng phiên bản đã cài (trong node_modules) thay vì trả lời theo trí nhớ, yêu cầu kèm file và số dòng, rồi tự mở ra đọc. Trí nhớ của mô hình có thể đúng cho một phiên bản cũ, nên câu trả lời trôi chảy vẫn có thể sai với bản bạn đang dùng.

    Nên kể một ví dụ thật, chẳng hạn một lần giả định sai về đơn vị thời gian cho ra ngày tháng 1 năm 1970, và bạn phát hiện nhờ chạy thử thay vì nhờ đọc.

    Điểm cộng: nói rằng bạn ghi lại những gì đã tìm hiểu (file nào làm gì, chỗ nào dễ sai) vào README hoặc ghi chú, để người sau không phải dò lại.

    Mở trang riêng của câu hỏi
  13. JuniorAI cho developerNhờ AI viết commit message từ git diff thì còn thiếu gì, và bạn kiểm tra kết quả thế nào?

    Người phỏng vấn muốn biết bạn dùng AI như một công cụ hỗ trợ có kiểm soát, và hiểu một commit message tốt nên chứa gì.

    Điều AI thiếu là lý do. Diff chỉ cho biết dòng nào được thêm hoặc xóa, tức là "đổi gì". Còn "vì sao đổi" nằm trong đầu bạn: lỗi nào người dùng gặp, vì sao chọn cách này thay vì cách khác. Nếu chỉ đưa diff, bạn thường nhận được kiểu message như "thêm bot.catch để xử lý lỗi", đúng theo nghĩa đen nhưng người đọc sau không biết chuyện gì đã xảy ra. Vì vậy mình luôn đưa thêm một hai câu ý định vào prompt.

    Mình kiểm tra kết quả theo ba bước. Thứ nhất, đọc từng câu và đối chiếu với diff: nếu message nói "đã thêm test" mà diff không có file test nào thì đó là lời bịa và phải xóa. Thứ hai, xem dòng đầu có đúng dạng của dự án không (ví dụ kiểu(phạm vi): mô tả, ngắn gọn, bắt đầu bằng động từ). Thứ ba, xem phần thân có giải thích vì sao thay vì kể lại từng dòng code.

    Một ý nữa là AI chỉ viết hay khi diff chỉ chứa một việc. Nếu bạn gộp spec, tính năng và test vào một commit thì không message nào mô tả tốt được. Nên tách commit theo ý nghĩa trước (git add -p giúp chọn từng đoạn thay đổi), rồi mới nhờ AI.

    Điểm cộng: nhắc rằng người chịu trách nhiệm cho nội dung commit là bạn chứ không phải công cụ, và với pull request thì mục "đã kiểm tra thế nào" phải do bạn điền từ kết quả thật.

    Mở trang riêng của câu hỏi
  14. JuniorBảo mậtXác thực (authentication) khác phân quyền (authorization) thế nào? Nên kiểm tra quyền ở đâu trong một ứng dụng web?

    Xác thực trả lời câu hỏi "bạn là ai", ví dụ đăng nhập bằng email và mật khẩu. Phân quyền trả lời câu hỏi "bạn được làm gì", ví dụ chỉ tác giả được sửa bài. Hai việc này tách rời nhau, và lỗi hay gặp nhất là coi việc thứ nhất như đã bao gồm việc thứ hai.

    Ví dụ cụ thể: một blog viết luật "ai đã đăng nhập thì được sửa bài", vì nghĩ rằng chỉ có tác giả mới có tài khoản. Nếu dịch vụ đăng nhập còn cho phép tự đăng ký, bất kỳ ai cũng tạo được tài khoản, đăng nhập hợp lệ, và sửa được bài. Người đó đã được xác thực đúng, nhưng chưa từng được phân quyền.

    Về chỗ kiểm tra quyền, câu trả lời tốt nêu được ba tầng và vai trò của từng tầng:

    1. Giao diện (ẩn nút, chuyển hướng): chỉ để người dùng thấy đúng thứ họ dùng được. Không bảo vệ gì, vì người ta gọi thẳng API.
    2. Server (API route, Server Action, middleware): mỗi điểm nhận request phải tự kiểm tra lại, không dựa vào việc người dùng đã đi qua trang nào.
    3. Database (ví dụ Row Level Security của PostgreSQL): lớp cuối cùng, vẫn đúng khi tầng trên viết thiếu một chỗ kiểm tra.

    Thêm hai ý cho thấy bạn đã làm thật:

    • Thông tin dùng để phân quyền phải nằm ở nơi người dùng không tự sửa được. Với Supabase, đó là app_metadata chứ không phải user_metadata.
    • Ở phía server, phải kiểm tra chữ ký của token chứ không chỉ đọc cookie, vì người dùng tự đặt được cookie trong trình duyệt của họ.

    Điểm cộng: nhắc tới mã 401 (chưa xác thực) và 403 (đã xác thực nhưng không có quyền), và việc quyền mới cấp hoặc mới thu hồi chỉ có hiệu lực khi token được làm mới.

    Mở trang riêng của câu hỏi
  15. JuniorDatabaseDatabase migration là gì? Thêm một cột NOT NULL vào bảng đang có dữ liệu thì cần lưu ý gì?

    Migration là một file (thường là SQL) mô tả một thay đổi của schema, ví dụ tạo bảng, thêm cột, thêm index. Các file được đặt tên theo thứ tự thời gian, commit vào Git và chạy lần lượt, nên database ở máy dev, staging và production luôn đi qua đúng cùng các bước. Thay đổi schema nhờ vậy cũng được review trong pull request như code.

    Với câu thêm cột NOT NULL, người phỏng vấn muốn nghe hai ý.

    Thứ nhất, alter table posts add column tags text[] not null sẽ lỗi trên bảng có dữ liệu: các dòng cũ không có giá trị cho cột mới, tức là null, vi phạm ràng buộc (PostgreSQL báo column "tags" of relation "posts" contains null values). Trên máy dev với bảng rỗng thì nó lại chạy được, nên lỗi chỉ lộ ra khi lên production. Cách sửa là cho giá trị mặc định: not null default '{}'. Nếu không có giá trị mặc định hợp lý, chia làm nhiều bước: thêm cột cho phép null, điền dữ liệu, rồi mới đặt not null.

    Thứ hai là tương thích ngược. Khi deploy, migration và code mới không đổi cùng một lúc. Thường có một khoảng thời gian bản code cũ chạy trên schema mới, nên migration không được làm hỏng bản cũ: không xóa hay đổi tên cột mà code cũ còn dùng, cột mới phải có mặc định để câu insert cũ vẫn chạy.

    Điểm cộng: nói rằng migration đã chạy trên production thì không sửa lại, muốn đổi thì viết migration mới.

    Mở trang riêng của câu hỏi
  16. JuniorDatabaseVì sao thường không lưu ảnh trực tiếp trong database? Bạn lưu ảnh ở đâu?

    Ảnh là dữ liệu lớn và hầu như không bao giờ cần truy vấn theo nội dung. Lưu thẳng vào database (dạng nhị phân hay base64) gây nhiều vấn đề:

    • Tốn dung lượng đắt nhất. Dung lượng database thường giới hạn và đắt hơn kho file. Ví dụ gói miễn phí của Supabase có 500 MB database: một bài viết dài chỉ vài chục KB chữ, trong khi một ảnh chụp điện thoại đã vài MB. Vài chục bài có ảnh là đầy.
    • Chậm và nặng. Mỗi truy vấn kéo theo dữ liệu lớn, sao lưu và khôi phục lâu hơn, bộ nhớ đệm của database bị chiếm chỗ.
    • Không tận dụng được CDN. Ảnh trong kho file (object storage) được phục vụ qua CDN, lưu đệm gần người dùng; ảnh trong database phải đi qua máy chủ ứng dụng.

    Cách làm phổ biến: lưu file ảnh ở object storage (Supabase Storage, Amazon S3, Cloudflare R2…), còn database chỉ lưu đường dẫn và thông tin mô tả (kích thước, mô tả alt, người tải lên). Thêm vào đó, nén ảnh trước khi tải lên (resize, đổi sang WebP/AVIF) để giảm cả dung lượng lưu trữ lẫn băng thông.

    Điểm cộng: nhắc tới việc dọn ảnh không còn được bài nào dùng, và phân quyền cho kho file, tức ai được tải lên, ai được xem.

    Mở trang riêng của câu hỏi
  17. JuniorDự án & kinh nghiệmBạn hãy giới thiệu về bản thân trong khoảng một phút.

    Câu này thường mở đầu buổi phỏng vấn, và người hỏi không cần nghe lại toàn bộ CV. Họ muốn biết trong một phút bạn chọn kể điều gì, và có kể rõ ràng được không. Câu trả lời tốt thường có bốn phần, mỗi phần một hai câu:

    1. Bạn là ai, làm gì: "Em là frontend developer mới tốt nghiệp, làm chủ yếu với React và TypeScript."
    2. Một bằng chứng cụ thể: một dự án, kèm phần khó nhất và kết quả. Ví dụ: "Em làm một app ghi chi tiêu bằng tin nhắn. Phần khó là đọc số tiền theo kiểu người Việt hay viết như 35k hay 1tr2, em giải quyết bằng một bảng ví dụ, mỗi dòng là một test. App có 21 test và vài bạn cùng lớp dùng hằng ngày."
    3. Điều bạn đang học hoặc muốn làm tốt hơn, nói cụ thể: "Em đang học viết test cho giao diện React."
    4. Vì sao bạn ở đây: nối với vị trí đang ứng tuyển, ví dụ công ty dùng React và TypeScript, sản phẩm có nhiều người dùng di động.

    Nên tránh:

    • Đọc lại CV theo thứ tự thời gian, từ trường cấp ba.
    • Dùng tính từ không kiểm chứng được như "năng động, ham học hỏi, chịu được áp lực".
    • Kể một dự án mà bạn không trả lời được câu hỏi tiếp theo về nó, vì người phỏng vấn thường hỏi sâu ngay vào dự án bạn vừa nhắc.

    Chuẩn bị trước: viết ra rồi tập nói thành lời vài lần, bấm giờ để giữ trong khoảng một phút. Nội dung nên khớp với tiêu đề và mục Giới thiệu trên LinkedIn của bạn, vì người phỏng vấn có thể vừa đọc chúng xong.

    Mở trang riêng của câu hỏi
  18. JuniorDự án & kinh nghiệmCuối buổi phỏng vấn, "Bạn có câu hỏi nào cho chúng tôi không?" — nên hỏi gì?

    Câu này không có đáp án đúng, nhưng có cách trả lời bỏ phí cơ hội: "dạ không ạ". Câu hỏi của bạn cho người phỏng vấn biết bạn quan tâm điều gì, và câu trả lời của họ giúp bạn quyết định có muốn làm ở đó không.

    Nên chuẩn bị sẵn 4–5 câu và chọn 2–3 câu tùy thời gian. Câu tốt thường gắn với một chi tiết trong tin tuyển dụng hoặc với điều người phỏng vấn vừa nói:

    • Về công việc: "Trong ba tháng đầu, một bạn junior thường làm những việc gì?", "Nhóm review code thế nào, một pull request thường mất bao lâu để được merge?"
    • Về việc học: "Người hướng dẫn gặp bạn mới bao lâu một lần?", "Theo anh/chị, một bạn junior làm tốt sau thử việc trông như thế nào?" Câu sau đặc biệt hữu ích, vì câu trả lời chính là tiêu chí bạn sẽ được đánh giá.
    • Về quy trình: "Sau buổi hôm nay còn vòng nào, và khi nào em có thể nhận phản hồi?"

    Nên tránh:

    • Câu mà tin tuyển dụng hoặc buổi nói chuyện đã trả lời rõ, vì nó cho thấy bạn không chú ý.
    • Hỏi về lương, ngày nghỉ ở vòng kỹ thuật với trưởng nhóm. Những chuyện này hỏi bộ phận tuyển dụng hoặc ở vòng sau.
    • Câu chung chung chép từ mạng mà bạn không thật sự quan tâm tới câu trả lời.

    Điểm cộng: hỏi tiếp một câu dựa trên điều người phỏng vấn vừa kể trong buổi, ví dụ "lúc nãy anh có nhắc nhóm đang chuyển sang Next.js, phần đó junior có được tham gia không?". Nó cho thấy bạn lắng nghe chứ không chỉ đọc câu đã chuẩn bị.

    Mở trang riêng của câu hỏi
  19. JuniorDự án & kinh nghiệmHãy kể về một dự án trong CV mà bạn tự hào nhất. Phần nào do chính bạn làm?

    Người phỏng vấn hỏi câu này để kiểm tra ba điều: dự án có thật sự do bạn làm không, bạn hiểu nó sâu đến đâu, và bạn có kể lại một việc kỹ thuật cho người khác hiểu được không.

    Một câu trả lời tốt dài khoảng 2 phút, theo thứ tự:

    1. Vấn đề: dự án giải quyết việc gì, cho ai. Một câu là đủ.
    2. Vai trò của bạn: với dự án nhóm, nói rõ phần nào bạn làm, ví dụ "mình làm toàn bộ phần đăng nhập và phân quyền". Đừng nói "chúng em làm" cho mọi thứ.
    3. Một quyết định kỹ thuật: bạn chọn cách làm nào, vì sao, và đã cân nhắc phương án nào khác. Ví dụ: "mình lưu ảnh trên storage và chỉ lưu đường dẫn trong database để không vượt giới hạn dung lượng".
    4. Một khó khăn và cách vượt qua: một lỗi cụ thể, bạn tìm ra nguyên nhân thế nào.
    5. Nếu làm lại: một điều bạn sẽ làm khác. Câu này cho thấy bạn biết tự nhìn lại.

    Nên tránh:

    • Kể lại danh sách tính năng như đọc README.
    • Nhận làm phần mà bạn không trả lời được câu hỏi tiếp theo về nó. Người phỏng vấn thường hỏi sâu thêm hai, ba tầng vào đúng phần bạn nhận.

    Chuẩn bị trước: với mỗi dự án trong CV, viết sẵn 5 ý trên và tập nói thành lời ít nhất một lần.

    Mở trang riêng của câu hỏi
  20. JuniorDự án & kinh nghiệmNếu được làm lại dự án cá nhân của mình từ đầu, bạn sẽ thay đổi điều gì?

    Câu này kiểm tra xem bạn có tự nhìn lại công việc của mình không. Trả lời "em thấy ổn rồi, không đổi gì" là câu yếu nhất, vì nó cho thấy bạn chưa học được gì từ dự án.

    Một câu trả lời tốt thường có ba phần:

    • Một quyết định cụ thể bạn sẽ đổi, kèm lý do. Ví dụ: "Em sẽ viết bảng ví dụ và test trước khi viết hàm đọc số tiền. Lần đầu em viết code trước, và một tin nhắn 'trà sữa 32K' bị đọc thành 32 tỷ."
    • Điều bạn sẽ giữ nguyên, để cho thấy bạn phân biệt được quyết định tốt và quyết định tệ. Ví dụ: "Em vẫn giữ việc từ chối khi tin nhắn có hai số tiền, vì ghi sai tiền tệ hơn bắt người dùng nhắn lại."
    • Bài học áp dụng được cho việc khác, như cắt phạm vi sớm, nghĩ tới múi giờ của máy chủ, hay hỏi người dùng thật sớm hơn.

    Tránh hai cái bẫy: chê công nghệ đã chọn mà không nêu được vì sao, và kể một thay đổi lớn đến mức nghe như đập đi làm lại toàn bộ.

    Điểm cộng: chuẩn bị trước câu này cho dự án đầu tiên trong CV, vì người phỏng vấn thường hỏi tiếp ngay sau câu "kể về dự án của bạn".

    Mở trang riêng của câu hỏi
  21. JuniorDự án & kinh nghiệmVì sao bạn ứng tuyển vào vị trí này ở công ty chúng tôi?

    Câu này kiểm tra một điều đơn giản: bạn có đọc tin tuyển dụng và tìm hiểu công ty không, hay đang gửi cùng một hồ sơ cho mọi nơi. Người hỏi không cần nghe lời khen công ty. Họ muốn thấy bạn nối được thứ họ cần với thứ bạn đã làm.

    Một câu trả lời tốt thường có ba ý, mỗi ý một hai câu:

    1. Một chi tiết cụ thể trong tin hoặc về sản phẩm. Ví dụ: "Tin ghi nhóm frontend viết test cho phần mình làm và có review code. Em đang tìm đúng một nơi như vậy."
    2. Bằng chứng bạn hợp với chi tiết đó. "Dự án cá nhân của em là một app ghi chi tiêu, viết bằng React và TypeScript, có 21 unit test. Em đã quen làm việc qua pull request trong đồ án nhóm."
    3. Điều bạn muốn học ở đây, nói thật và cụ thể: "Em chưa từng làm việc với một codebase lớn có nhiều người cùng sửa, và em muốn học cách làm đó từ nhóm."

    Nên tránh:

    • Câu chung chung dùng được cho mọi công ty: "môi trường năng động, chuyên nghiệp", "công ty lớn, nhiều cơ hội".
    • Chỉ nói về thứ bạn nhận được (lương, gần nhà) mà không nói bạn đóng góp gì. Những lý do đó hoàn toàn chính đáng, nhưng không nên là ý duy nhất.
    • Nói sai về sản phẩm của công ty. Nếu chưa dùng thử được, hãy nói mình đã đọc gì về nó.

    Chuẩn bị trước: đọc lại tin tuyển dụng, gạch chân hai yêu cầu bạn có bằng chứng rõ nhất, và dành mười phút xem trang web hoặc sản phẩm của công ty. Giữ lại bản CV bạn đã gửi cho đúng nơi này, vì người phỏng vấn sẽ hỏi dựa trên nó.

    Điểm cộng: kết thúc bằng một câu hỏi ngược lại về công việc hằng ngày, ví dụ nhóm đang review code theo cách nào.

    Mở trang riêng của câu hỏi
  22. JuniorGitGit merge và git rebase khác nhau thế nào? Khi nào nên dùng cái nào?

    Cả hai đều đưa thay đổi từ nhánh này sang nhánh khác, nhưng ghi lại lịch sử theo hai cách khác nhau.

    • git merge tạo một merge commit nối hai nhánh. Lịch sử giữ nguyên, thấy rõ nhánh đã tách và nhập lại khi nào.
    • git rebase viết lại các commit của nhánh bạn lên trên đầu nhánh đích. Lịch sử thành một đường thẳng, dễ đọc hơn, nhưng mã hash của các commit thay đổi.
    Cập nhật nhánh feature từ main
    git switch feat/login
    git fetch origin
    git rebase origin/main   # lịch sử thẳng, chỉ làm trên nhánh của riêng bạn

    Trong phỏng vấn, nói thêm được đánh đổi (lịch sử thật so với lịch sử dễ đọc) và quy tắc của team bạn đang dùng sẽ ghi điểm hơn là chỉ định nghĩa.

    Mở trang riêng của câu hỏi
  23. JuniorGitKhi nhận code review mà bạn không đồng ý với một góp ý, bạn làm gì?

    Người phỏng vấn hỏi câu này để xem bạn làm việc nhóm thế nào, không phải để xem bạn thắng tranh luận. Một câu trả lời tốt cho thấy bạn tách được cảm xúc khỏi kỹ thuật, và bạn quyết định dựa trên bằng chứng.

    Bạn có thể trả lời theo bốn bước:

    1. Hiểu đúng góp ý trước đã. Đọc lại, và nếu chưa rõ thì hỏi lại người review đang lo điều gì. Nhiều khi mình không đồng ý chỉ vì hiểu sai ý họ.
    2. Thử điều họ đề nghị. Chạy thử thay đổi đó, chạy test. Kết quả thường tự trả lời: hoặc góp ý đúng và bạn sửa theo, hoặc nó làm hỏng một trường hợp cụ thể.
    3. Trả lời bằng dữ liệu và kết thúc bằng câu hỏi. Ví dụ: "Em thử cách này rồi, 4 test từ chối bị đỏ vì '35,000' bị đọc thành 35000 mà không báo lỗi. Em muốn giữ cách cũ, anh thấy ổn không?". Câu hỏi để ngỏ khả năng người review biết điều bạn chưa biết.
    4. Không xong sau hai lượt thì nói chuyện trực tiếp. Mười bình luận qua lại tốn thời gian hơn năm phút gọi nhau. Nếu vẫn chưa thống nhất, theo quy ước của nhóm hoặc nhờ một người thứ ba.

    Nên nêu thêm hai ý. Một là phân biệt lỗi thật với sở thích cá nhân: với chuyện sở thích, mình nói rõ lý do của mình nhưng sẵn sàng theo cách của nhóm. Hai là góp ý đúng nhưng ngoài phạm vi thì ghi thành việc riêng, không nhồi vào pull request đang review.

    Điểm cộng: kể một lần cụ thể bạn từng không đồng ý, bạn đã thử gì, và kết quả ra sao, kể cả khi người review đúng.

    Mở trang riêng của câu hỏi
  24. JuniorGitVì sao không nên commit thẳng lên nhánh main? Một pull request tốt cần có những gì?

    Commit thẳng lên main nghĩa là thay đổi trở thành code chung ngay lập tức, trước khi có ai đọc và trước khi kiểm tra tự động (CI) chạy. Nếu main còn nối với việc deploy, một lỗi nhỏ sẽ lên thẳng production. Làm trên một nhánh riêng thì main luôn ở trạng thái chạy được, nhiều người làm song song không giẫm lên nhau, và mỗi thay đổi có một chỗ để thảo luận.

    Với phần thứ hai, người phỏng vấn muốn nghe bạn nghĩ cho người review. Một pull request tốt thường có:

    • Một mục đích duy nhất. Sửa một lỗi hoặc thêm một tính năng. PR trộn sửa lỗi với đổi tên hàng loạt và nâng thư viện thì rất khó review, và khó gỡ ra khi có sự cố.
    • Commit chia theo ý nghĩa, tên rõ ràng. Ví dụ một commit cho bản sửa, một commit cho test. File tự sinh như package-lock.json nằm cùng commit với thay đổi sinh ra nó.
    • Mô tả trả lời bốn câu: vấn đề là gì, đã đổi gì, kiểm tra bằng cách nào (lệnh cụ thể), và điều gì cố ý chưa làm.
    • Đã tự review trước. Đọc lại diff của chính mình để bỏ console.log để quên, file cấu hình cá nhân, code thử nghiệm.
    • Kích thước vừa phải. Vài chục tới vài trăm dòng có ý nghĩa. PR nghìn dòng thường chỉ nhận được một chữ "LGTM" mà không ai thật sự đọc.

    Nên kể một ví dụ của chính bạn, chẳng hạn một lần đọc lại diff và bắt được dòng log in ra số điện thoại khách hàng trước khi mở PR.

    Điểm cộng: nói rằng nhiều nhóm bật branch protection để chặn push thẳng lên main và bắt buộc CI xanh trước khi merge, và rằng PR nhỏ giúp tìm ra commit gây lỗi nhanh hơn.

    Mở trang riêng của câu hỏi
  25. JuniorHTTP & WebGET và POST khác nhau ra sao? Vì sao không nên dùng GET để xóa dữ liệu?

    GET dùng để đọc dữ liệu: tham số nằm trên URL, có thể được cache, lưu lịch sử và được trình duyệt hay công cụ quét tự gọi lại (prefetch, xem trước liên kết). POST dùng để gửi dữ liệu làm thay đổi trạng thái, dữ liệu nằm trong body.

    GET phải an toàn (không thay đổi gì) và idempotent (gọi nhiều lần cũng như một lần). Nếu một link GET xóa dữ liệu, chỉ cần một bot quét link hay trình duyệt prefetch là dữ liệu mất.

    Mở trang riêng của câu hỏi
  26. JuniorJavaScriptBreakpoint khác console.log thế nào khi debug? Khi nào bạn vẫn dùng console.log?

    Breakpoint dừng chương trình ở một dòng, cho bạn xem mọi biến đang có giá trị gì và kiểu gì, xem ai đã gọi hàm (call stack), và gõ thêm biểu thức để tính ngay trong ngữ cảnh đó. console.log chỉ in ra thứ bạn đoán trước là cần in.

    Một ví dụ cho thấy khác biệt: giá món ăn được lưu nhầm thành chuỗi "35.000". console.log(item.price) in ra 35.000, nhìn y như ba mươi lăm nghìn. Trong khung Variables của debugger, giá trị hiện kèm dấu nháy, và typeof trả về 'string'. Lỗi lộ ra ngay: "35.000" * 2 bằng 70 vì JavaScript đọc dấu chấm là dấu thập phân.

    Các công cụ nên kể được: breakpoint có điều kiện (chỉ dừng khi order.id === 1044), Watch, Debug Console, Call Stack, và logpoint (in log mà không dừng, không sửa code).

    console.log vẫn hợp lý khi:

    • viết script nhỏ chạy một lần;
    • lỗi phụ thuộc thời gian, việc dừng chương trình làm đổi thứ tự chạy;
    • trên production, nơi không gắn debugger được, và lúc đó nên ghi log có cấu trúc.

    Điểm cộng: biết log bằng JSON.stringify(value) hoặc { value } để thấy rõ chuỗi hay số.

    Mở trang riêng của câu hỏi
  27. JuniorJavaScriptKhác nhau giữa var, let và const trong JavaScript?
    • var có phạm vi hàm, bị hoisting (khai báo được đưa lên đầu hàm với giá trị undefined) và cho khai báo lại.
    • let có phạm vi khối { }, không cho khai báo lại trong cùng khối, và không dùng được trước dòng khai báo.
    • const giống let nhưng không gán lại được. Lưu ý: object hay mảng khai báo bằng const vẫn sửa được bên trong.
    const không có nghĩa là bất biến
    const user = { name: "An" };
    user.name = "Bình"; // được
    user = {};          // TypeError

    Thói quen tốt: mặc định dùng const, chỉ dùng let khi thật sự cần gán lại, và tránh var trong code mới.

    Mở trang riêng của câu hỏi
  28. JuniorJavaScriptStack trace là gì? Khi gặp "Cannot read properties of undefined", bạn tìm nguyên nhân thế nào?

    Stack trace là danh sách các hàm đang được gọi dở tại thời điểm lỗi xảy ra, kèm tên file, số dòng và số cột của từng lời gọi. Trong JavaScript, khung trên cùng là nơi lỗi xảy ra; các khung bên dưới là đường đi dẫn tới đó.

    Với lỗi TypeError: Cannot read properties of undefined (reading 'name'), một câu trả lời tốt đi theo các bước:

    1. Đọc thông điệp: thuộc tính đang đọc là name, nên thứ đứng ngay trước .name đang là undefined.
    2. Tìm khung đầu tiên thuộc code của mình: bỏ qua các khung trong node_modules hay node:internal, mở đúng file, dòng và cột.
    3. Lần ngược nguồn dữ liệu: dòng báo lỗi là nơi lỗi lộ ra, chưa chắc là nơi bắt đầu. Đi xuống các khung để xem giá trị undefined được truyền từ đâu: API chưa trả về, state ban đầu rỗng, hay dữ liệu thiếu trường.
    4. Tái hiện bằng ví dụ nhỏ nhất, rồi mới sửa đúng chỗ: sửa dữ liệu ở nguồn, hoặc xử lý trường hợp hợp lệ bằng giá trị mặc định hay ?..

    Điểm cộng nếu bạn nhắc thêm: khi bắt lỗi rồi ném lại, dùng new Error("…", { cause: err }) để không mất lỗi gốc; còn với code production đã nén, cần source map mới đọc được stack trace.

    Mở trang riêng của câu hỏi
  29. JuniorKỹ năng lập trìnhKhi nào ghi log ở mức warn, khi nào ở mức error, và không nên ghi gì vào log?

    Người phỏng vấn muốn biết bạn có xem log như công cụ để người đến sau tìm nguyên nhân, và có biết phân loại sự kiện để lỗi thật không bị chìm hay không.

    Cách phân loại đơn giản và dễ nhớ là hỏi "lỗi của ai". Mức info là chuyện bình thường, ví dụ một đơn hàng được nhận. Mức warn là người gọi gửi sai, như request bị từ chối vì dữ liệu không hợp lệ (mã 4xx): máy chủ làm đúng việc của nó. Mức error là máy chủ làm sai hoặc gặp tình huống không lường trước (mã 5xx), và cần có người xem.

    Lý do phải tách: nếu mọi thứ đều ghi error, một website gửi sai năm trăm đơn sẽ tạo năm trăm dòng đỏ che mất một lỗi thật duy nhất của máy chủ. Lọc theo error lúc hai giờ sáng chỉ hữu ích khi mức error có ý nghĩa.

    Một dòng log tốt trả lời được bốn câu: chuyện gì, lúc nào, của request nào, và quan trọng đến đâu. Vì vậy mỗi request nên có một mã riêng (request id) gắn vào mọi dòng của nó. Nếu bạn bọc một lỗi bằng lỗi khác, hãy giữ lỗi gốc trong cause để log vẫn nói được nguyên nhân.

    Về những gì không nên ghi: dữ liệu cá nhân như tên, số điện thoại, và bí mật như mật khẩu, token, khóa API. File log hay được chép sang nhiều nơi, nên thứ gì đã vào log coi như đã lộ ra ngoài dự án. Khi từ chối một request vì dữ liệu sai, hãy ghi tên trường và lý do, không ghi giá trị người dùng gửi lên.

    Điểm cộng: nhắc rằng không nên nuốt lỗi bằng khối catch rỗng, và khi trả lỗi 500 cho người gọi thì chỉ đưa một mã để tra, còn chi tiết nội bộ ở lại trong log.

    Mở trang riêng của câu hỏi
  30. JuniorKỹ năng lập trìnhRefactor là gì, làm sao chắc rằng sau khi refactor code vẫn chạy y như cũ?

    Người phỏng vấn muốn biết bạn có phân biệt được sửa cấu trúc với sửa hành vi không, và bạn có thói quen tự bảo vệ mình bằng test không. Refactor là thay đổi cấu trúc bên trong của code để dễ đọc, dễ sửa hơn mà không đổi hành vi bên ngoài. Đổi tên biến, tách một hàm dài thành các hàm nhỏ, gom đoạn code lặp vào một chỗ đều là refactor. Thêm tính năng hay sửa lỗi thì không phải, vì chúng đổi hành vi.

    Cách trả lời tốt đi theo thứ tự này:

    1. Có test trước khi động vào code. Nếu code chưa có test, viết test đặc tả: một test chụp lại code đang làm gì, kể cả khi hành vi đó hơi kỳ. Nó không hỏi "đúng hay sai", chỉ hỏi "sau khi sửa có khác trước không".
    2. Đi từng bước nhỏ, chạy test sau mỗi bước. Test đỏ thì biết ngay bước nào vừa gây ra, vì chỉ có một thay đổi nhỏ.
    3. Không trộn refactor với sửa hành vi. Nếu thấy một lỗi giữa chừng, ghi lại và sửa ở commit riêng. Tách commit đổi tên khỏi commit di chuyển code cũng giúp người review đọc nhanh hơn.
    4. Biết cái gì là "tên bên trong", cái gì là "tên bên ngoài". Đổi tên một biến cục bộ thì an toàn. Đổi tên một trường trong JSON mà API trả về thì làm hỏng mọi nơi đang đọc trường đó, dù code của bạn vẫn biên dịch được.

    Ví dụ cụ thể giúp câu trả lời thuyết phục hơn. Chẳng hạn bạn đang dọn một máy chủ nhỏ trả danh sách đơn hàng. Bạn đổi trường khach thành customer cho nhất quán với phần còn lại, và test đặc tả kiểm tra phản hồi của máy chủ lập tức đỏ, vì trang quản lý đang đọc đúng trường khach. Không có test đó thì lỗi chỉ lộ ra khi người dùng thấy cột trống.

    Điểm cộng: nói rõ khi nào bạn không refactor, ví dụ khi sắp có deadline mà code đó không phải chỗ đang sửa, hoặc khi chưa thể viết test cho nó. Refactor đúng lúc, trong phạm vi nhỏ, hợp với một pull request dễ review hơn là dọn cả dự án trong một lần.

    Mở trang riêng của câu hỏi
  31. JuniorNext.jsISR trong Next.js là gì, và khác gì so với trang tĩnh hoặc render ở mỗi request?

    Người phỏng vấn muốn biết bạn hiểu ba cách dựng trang và biết chọn cách phù hợp với từng loại nội dung, không chỉ thuộc tên viết tắt.

    Có ba cách. Trang tĩnh (SSG) được dựng một lần lúc build và giữ nguyên, nên rất nhanh nhưng chỉ đổi khi deploy lại. Render ở mỗi request (SSR, hay dynamic) dựng trang mới mỗi lần có người xem, nên luôn mới nhưng mỗi lượt xem đều tốn một lần xử lý và truy vấn dữ liệu. ISR (incremental static regeneration) nằm giữa: trang được lưu như trang tĩnh, và Next.js dựng lại nó ở nền theo một trong hai cách.

    1. Theo thời gian. Đặt export const revalidate = 60 thì sau 60 giây, request kế tiếp vẫn nhận bản cũ nhưng kích hoạt việc dựng lại. Khi bản mới xong, các request sau nhận bản mới. Nghĩa là người đọc không bao giờ phải chờ dữ liệu.
    2. Theo yêu cầu. Khi nội dung đổi, code gọi revalidatePath hoặc revalidateTag để đánh dấu cache là hết hạn, và lần truy cập kế tiếp dựng lại.

    Chọn thế nào: nội dung ít đổi mà nhiều người đọc, như bài blog hay trang sản phẩm, hợp với ISR. Nội dung riêng cho từng người dùng, như trang tài khoản, cần render ở mỗi request. Nội dung không bao giờ đổi, như trang giới thiệu, hợp với trang tĩnh.

    Hai điểm nên nêu thêm. Một là đánh đổi: ISR cho phép người đọc thấy bản cũ trong một khoảng ngắn, nên nó không hợp với dữ liệu phải chính xác từng giây như tồn kho hay số dư. Hai là nếu dùng route để làm mới theo yêu cầu thì phải bảo vệ nó bằng một mã bí mật, nếu không ai cũng gọi được và buộc máy chủ dựng lại liên tục.

    Điểm cộng: nói được ISR cần một máy chủ Node.js chạy được, nên không dùng được khi xuất blog thành file HTML tĩnh hoàn toàn.

    Mở trang riêng của câu hỏi
  32. JuniorNext.jsSitemap, thẻ canonical và JSON-LD dùng để làm gì trong SEO kỹ thuật?

    Người phỏng vấn muốn biết bạn hiểu mỗi thành phần giải quyết vấn đề gì, thay vì chỉ biết tên và chép cấu hình.

    Sơ đồ trang (sitemap) là một file XML liệt kê các địa chỉ trong site, giúp công cụ tìm kiếm tìm thấy trang mà không cần đi theo từng liên kết. Hướng dẫn của Google nói họ bỏ qua priority và changefreq, và chỉ dùng lastmod khi giá trị đó luôn khớp với lần sửa thật của trang. Mọi giá trị trong file phải được escape ký tự đặc biệt của XML.

    Thẻ canonical nói địa chỉ nào là bản chính của một nội dung. Khi cùng một bài có thể truy cập bằng nhiều địa chỉ (có tham số theo dõi, có hay không dấu gạch chéo cuối), canonical giúp công cụ tìm kiếm gom chúng về một. Với trang có nhiều ngôn ngữ, mỗi bản trỏ canonical về chính nó và liệt kê các bản kia bằng hreflang.

    JSON-LD là khối dữ liệu có cấu trúc (từ vựng schema.org), nói rõ trang là một bài viết, tác giả là ai, đăng ngày nào. Khi nhúng vào thẻ script, cần thay ký tự < bằng <, vì JSON.stringify không làm sạch chuỗi và một tiêu đề chứa </script> có thể thoát khỏi thẻ.

    Một lưu ý thực tế: không thành phần nào bảo đảm lên top, chúng chỉ giúp công cụ tìm kiếm hiểu đúng trang. Môi trường xem trước (preview) cần noindex, và chỉ production mới được index.

    Điểm cộng: nhắc rằng sitemap cũng có thể sai theo cách khó thấy, ví dụ bị phục vụ như file tĩnh nên không cập nhật khi đăng bài mới, và nên có cách kiểm tra việc đó trên môi trường thật.

    Mở trang riêng của câu hỏi
  33. JuniorNext.jsTrong Next.js App Router, nhóm route (folder) và thư mục riêng tư _folder khác nhau thế nào? Khi nào dùng cái nào?

    Cả hai đều là quy ước đặt tên thư mục trong app, nhưng giải quyết hai vấn đề khác nhau.

    Nhóm route là thư mục có tên trong ngoặc tròn, như (site) hay (shop). Next.js dùng nó để gom các trang lại nhưng không đưa tên nhóm vào URL: app/(site)/bai-viet/page.tsx vẫn là /bai-viet. Công dụng chính là cho một nhóm trang dùng chung một layout riêng. Ví dụ, trang công khai của blog cần header, footer và được tạo sẵn, còn trang quản trị cần khung riêng và tạo mới ở mọi lượt truy cập. Đặt trang công khai vào (site) có layout riêng thì hai bên tách bạch mà URL không đổi.

    Thư mục riêng tư có tên bắt đầu bằng gạch dưới, như _components. Next.js bỏ qua thư mục này và mọi thư mục con khi tạo đường dẫn, nên nó dùng để đặt code không phải trang (component, hàm phụ) ngay cạnh trang.

    Một điểm hay bị hỏi thêm: ngay cả không có gạch dưới, file trong app cũng không thành trang nếu không phải page hoặc route. Vì vậy _folder chủ yếu để nói rõ ý đồ và tránh trùng tên với các file đặc biệt của Next.js.

    Điểm cộng: nhắc tới việc hai layout có thể cùng ứng với / (layout gốc và layout của nhóm) nhưng bọc phạm vi khác nhau.

    Mở trang riêng của câu hỏi
  34. JuniorPhỏng vấnTrong buổi phỏng vấn live coding, bạn làm gì trước khi viết dòng code đầu tiên?

    Người phỏng vấn hỏi câu này để xem bạn có quy trình hay chỉ gõ theo phản xạ. Câu trả lời tốt cho thấy bạn dành vài phút đầu để hiểu đề, vì đó là phần rẻ nhất của cả buổi.

    Bạn có thể trả lời theo ba bước:

    1. Nhắc lại đề bằng lời của mình và hỏi những chỗ mơ hồ. Ví dụ với đề "đếm ghi chú xuất hiện nhiều nhất": "Chữ hoa và chữ thường có tính là một không? Hai ghi chú bằng số lần thì xếp thế nào? Mảng có thể rỗng hay có phần tử trống không?" Câu hỏi của bạn cho thấy bạn nhìn ra được chỗ đề chưa rõ, và câu trả lời chốt luôn các luật của bài.
    2. Đi qua một ví dụ bằng tay, vừa đi vừa nói. Cách này thường làm lộ ra một luật mà bạn chưa xử lý, trước khi bạn viết một dòng code.
    3. Nói cách làm trước khi viết. Một hai câu, ví dụ "em sẽ chuẩn hóa mỗi ghi chú, đếm bằng một Map rồi sắp xếp giảm dần". Nếu hướng đó lệch, người phỏng vấn có cơ hội chỉnh bạn trước khi bạn mất mười phút.

    Nên nêu thêm lý do: hiểu sai đề thì phần code phía sau là công cốc, và người phỏng vấn đánh giá cách bạn nghĩ chứ không chỉ kết quả. Nếu bạn bí thì nói ra mình đang kẹt ở đâu và thử hướng nào, thay vì im lặng.

    Điểm cộng: kể một lỗi mà bước đi qua ví dụ bằng tay đã giúp bạn bắt được sớm, chẳng hạn code so sánh chuỗi nguyên văn nên "Cà phê " và "cà phê" bị đếm thành hai ghi chú.

    Mở trang riêng của câu hỏi
  35. JuniorSự nghiệpCâu hỏi hành vi là gì, và phương pháp STAR giúp trả lời nó thế nào?

    Người phỏng vấn muốn biết bạn có kể được một chuyện thật, có cấu trúc, và nói rõ chính bạn đã làm gì, thay vì trả lời chung chung.

    Câu hỏi hành vi là câu hỏi dạng "kể về một lần bạn…": làm việc nhóm gặp mâu thuẫn, gặp lỗi khó, mắc sai lầm. Người hỏi muốn dùng hành vi trong quá khứ để đoán cách bạn sẽ làm việc sau này. Không có đáp án đúng để kiểm tra bằng test, nên cách bạn kể chuyện quyết định ấn tượng.

    STAR là khung bốn phần để kể:

    1. Situation (Tình huống): bối cảnh ngắn, đủ để hiểu chuyện gì đang xảy ra.
    2. Task (Nhiệm vụ): trách nhiệm của bạn trong tình huống, tức vì sao chuyện này là việc của bạn.
    3. Action (Hành động): những bước cụ thể bạn đã làm, dùng "tôi" hay "em" thay vì "chúng tôi", vì người nghe đang đánh giá bạn.
    4. Result (Kết quả): chuyện xảy ra sau đó, tốt nhất đo được bằng con số, kèm điều bạn rút ra.

    Lỗi hay gặp là kể quá dài phần bối cảnh rồi nói về Hành động quá ngắn, nói về cả nhóm thay vì phần của mình, và bỏ quên Kết quả hoặc nêu một kết quả mơ hồ như "mọi chuyện ổn hơn". Một cách chuẩn bị tốt là có sẵn khoảng ba chuyện thật, mỗi chuyện ghi bốn dòng STAR, rồi chọn chuyện gần nhất với câu hỏi thay vì nghĩ chuyện mới.

    Ý quan trọng để nêu: đừng học thuộc câu chữ, và đừng bịa số liệu. Bạn chỉ cần nhớ bốn dòng của mỗi chuyện, nói thành lời mỗi lần một cách khác, và dùng con số bạn thật sự có.

    Điểm cộng: nhắc rằng với câu hỏi về điểm yếu hay sai lầm, bạn kể một việc thật cùng một việc bạn đã làm để sửa, thay vì một "điểm yếu" nghe an toàn như "em quá cầu toàn".

    Mở trang riêng của câu hỏi
  36. JuniorSự nghiệpMức lương bạn mong muốn là bao nhiêu, và bạn trả lời thế nào khi offer thấp hơn kỳ vọng?

    Người phỏng vấn muốn biết bạn có chuẩn bị, có hiểu giá trị của mình, và biết nói chuyện lương một cách bình tĩnh, không né tránh cũng không đòi vô lý.

    Trước khi trả lời, bạn cần làm bài tập về nhà. Nếu tin tuyển dụng đã ghi khoảng lương thì đó là dữ liệu tốt nhất: bạn biết công ty sẵn sàng trả trong khoảng nào. Nếu không, hãy tham khảo mức phổ biến cho vị trí, cấp độ và địa điểm của bạn từ các báo cáo lương và tin tuyển dụng tương tự. Công ty thường xây lương theo khoảng và theo bậc, nên có giới hạn mà người tuyển dụng không vượt được.

    Khi được hỏi, bạn nên nêu một khoảng có cơ sở, gắn với những gì bạn mang lại (dự án, test, kỹ năng khớp với yêu cầu), thay vì một con số trơn hay lý do cá nhân như tiền trọ. Nếu chưa muốn nói con số, có thể hỏi lại khoảng lương của vị trí rồi trả lời trong khoảng đó.

    Khi nhận offer thấp hơn kỳ vọng, đừng nhận hay từ chối ngay trong cuộc gọi. Cảm ơn, nói rõ bạn quan tâm, xin thư mời bằng văn bản và một hai ngày để cân nhắc. Đọc kỹ các mục như lương chính thức, phần trăm lương thử việc, ngày kết thúc thử việc, lương đóng bảo hiểm, thưởng và ngày xem xét lại lương, rồi phản hồi bằng email lịch sự với đề nghị cụ thể và lý do. Ở Việt Nam, lương thử việc phải ít nhất bằng 85% lương của công việc đó theo Bộ luật Lao động 2019.

    Nếu công ty không tăng được lương, bạn có thể hỏi thêm về những thứ khác như ngày xem xét lại lương ghi bằng văn bản, lịch học việc, hay số ngày làm tại nhà.

    Điểm cộng: nhắc rằng so sánh các phương án bằng tổng thu nhập năm đầu giúp thấy điều nào đáng đề nghị, và rằng thương lượng lịch sự thường là chuyện bình thường trong tuyển dụng.

    Mở trang riêng của câu hỏi
  37. JuniorSự nghiệpNếu được nhận, tuần đầu đi làm bạn sẽ làm những gì?

    Người phỏng vấn muốn biết bạn có kế hoạch thực tế cho giai đoạn đầu, biết ưu tiên học cách nhóm làm việc, và không hứa những điều không thể.

    Một câu trả lời tốt chia thành vài mục tiêu cụ thể, thay vì nói chung chung kiểu "em sẽ học hỏi nhiều".

    Thứ nhất, chạy được dự án trên máy của mình trong vài ngày đầu, và ghi lại những chỗ tài liệu hướng dẫn thiếu hoặc sai. Một người mới thấy những chỗ vấp mà người cũ không còn nhìn ra, và sửa chúng có ích cho cả nhóm.

    Thứ hai, hiểu cách nhóm làm việc: quy ước đặt tên nhánh, pull request cần mấy người duyệt, kiểm tra tự động nào phải xanh, ai review. Cách nhanh là đọc các pull request đã đóng gần đây.

    Thứ ba, nhận một việc nhỏ đầu tiên, như sửa tài liệu hay một lỗi nhỏ đã được chọn sẵn, để học quy trình mà không có áp lực của một bài toán khó.

    Thứ tư, hẹn người hướng dẫn một buổi góp ý giữa kỳ và hỏi họ mong đợi gì ở mình ở các mốc của thời gian thử việc. Bạn cũng có thể nói mình giữ một cuốn nhật ký ngắn cuối ngày để ghi điều chưa hiểu và điều đã thử trước khi hỏi.

    Điểm cộng: nhắc rằng thời gian thử việc tính theo ngày lịch nhưng số ngày làm việc ít hơn, nên mục tiêu cần đúng cỡ, và bạn chưa cần biết hết mã nguồn trong tuần đầu.

    Mở trang riêng của câu hỏi
  38. JuniorTestingUnit test là gì? Làm sao biết một test bạn viết thật sự có tác dụng?

    Unit test kiểm tra một phần nhỏ của code (một hàm, một module) một cách tự động: với đầu vào này, kết quả phải là kia. Nó chạy nhanh, không cần bật máy chủ hay database, nên có thể chạy ở mỗi lần lưu file và trước mỗi lần merge.

    Phần thứ hai của câu hỏi mới là phần người phỏng vấn muốn nghe. Một test có tác dụng khi nó đỏ lúc code sai. Vài dấu hiệu nên nêu:

    • Kết quả mong đợi đến từ bên ngoài code. Viết expect(orderTotal(don)).toBe(70000) vì bạn biết 2 ly × 35.000 là 70.000. Đừng tính lại kết quả mong đợi bằng đúng công thức đang được kiểm tra, vì khi đó test so sánh code với chính nó và luôn xanh.
    • Thử làm hỏng code. Sau khi sửa một lỗi, tạm bỏ bản sửa và chạy lại test. Test vẫn xanh nghĩa là nó chưa kiểm tra đúng chỗ.
    • Có cả trường hợp bị từ chối, không chỉ trường hợp chạy đúng. Ví dụ một hàm đọc giá phải báo lỗi với "35,000" thay vì đoán.
    • Tên test kể được chuyện gì đang được bảo vệ, như "2 ly cà phê muối giá 35.000 là 70.000 đồng", để người làm đỏ test hiểu ngay mà không cần hỏi.

    Điểm cộng: nói rằng test cho từng hàm nhỏ là chưa đủ. Nên có thêm test đi qua cả đường dữ liệu thật (đọc dữ liệu thô → chuẩn hóa → tính tổng), vì lỗi hay nằm ở chỗ một hàm đúng nhưng không được gọi.

    Mở trang riêng của câu hỏi
  39. JuniorTypeScriptVì sao phải kiểm tra dữ liệu đầu vào ở phía server dù TypeScript đã có kiểu?

    Người phỏng vấn muốn biết bạn có phân biệt được kiểu lúc biên dịch với dữ liệu thật lúc chạy, và có nghĩ tới việc dữ liệu từ bên ngoài không đáng tin hay không.

    Kiểu TypeScript chỉ tồn tại khi bạn viết và biên dịch code. Khi chương trình chạy, kiểu đã bị xóa, nên JSON.parse(body) trả về bất cứ thứ gì người gửi đưa vào, và việc viết as Order chỉ là bạn tự hứa với trình biên dịch, không kiểm tra gì cả. Dữ liệu từ request, từ file, từ API khác đều là dữ liệu không tin cậy cho tới khi được kiểm tra.

    Hậu quả của việc không kiểm tra:

    1. Lỗi lặng lẽ. Số lượng là chữ làm phép nhân ra NaN, rồi thành null khi chuyển sang JSON, và tổng tiền sai mà không có lỗi nào.
    2. Lỗi lan rộng. Một bản ghi thiếu trường làm hàm xử lý ném lỗi, và nếu hàm đó chạy trên cả danh sách thì một dữ liệu hỏng làm hỏng mọi người.
    3. Lỗ hổng bảo mật. Người gửi có thể đặt cả những trường họ không được phép đặt, như id hay quyền hạn.

    Cách làm đúng là kiểm tra ngay ở nơi dữ liệu đi vào hệ thống, bằng một thư viện schema như Zod: khai báo hình dạng hợp lệ, gọi safeParse, trả lỗi 400 kèm danh sách lỗi theo từng trường nếu sai. Sau bước đó, phần còn lại của code dùng kiểu do schema suy ra và tin vào nó. Nên khai báo dữ liệu nhận về là unknown thay vì any, để trình biên dịch buộc bạn kiểm tra trước khi dùng.

    Ý quan trọng để nêu: kiểm tra một lần ở cửa, không rải khắp nơi, và phân biệt lỗi do người gửi (400) với lỗi của máy chủ (500).

    Điểm cộng: nhắc tới việc giới hạn kích thước nội dung gửi lên, và chọn có cho phép trường lạ hay không (từ chối hẳn thường an toàn hơn lặng lẽ bỏ qua).

    Mở trang riêng của câu hỏi
  40. JuniorWebVì sao nên nén ảnh trước khi lưu, và nén ở trình duyệt hay ở máy chủ?

    Người phỏng vấn muốn biết bạn hiểu ảnh ảnh hưởng thế nào đến dung lượng lưu trữ và tốc độ tải trang, và có nghĩ tới các chi tiết dễ sai khi xử lý ảnh hay không.

    Lý do chính là ảnh gốc từ điện thoại hay ảnh chụp màn hình thường nặng vài MB đến hàng chục MB, trong khi trang chỉ hiển thị chúng ở chiều rộng vài trăm điểm ảnh. Lưu nguyên bản làm đầy kho lưu trữ nhanh (gói miễn phí thường chỉ có vài trăm MB đến vài GB) và bắt người đọc trên mạng di động tải một lượng dữ liệu họ không cần. Phần giảm nhiều nhất thường đến từ việc thu nhỏ kích thước về mức đủ dùng, ví dụ 1600 px cạnh dài để vẫn nét trên màn hình retina. Đổi định dạng sang WebP hay AVIF giảm thêm, nhưng mức giảm phụ thuộc vào ảnh nên cần đo trên ảnh thật.

    Còn chọn nơi nén thì có hai hướng. Nén ở trình duyệt (vẽ lại ảnh lên canvas) tiết kiệm băng thông lúc tải lên và không tốn tài nguyên máy chủ, hợp với trường hợp tác giả đáng tin. Nén ở máy chủ (ví dụ bằng sharp) hợp khi bạn không tin người tải lên, hoặc cần nhiều kích thước cho nhiều màn hình, nhưng tốn thời gian và bộ nhớ của hàm xử lý. Thành phần ảnh của framework (như next/image) chỉ tối ưu lúc phục vụ, còn ảnh gốc vẫn nằm trong kho.

    Những chi tiết dễ sai: ảnh chụp dọc có thẻ EXIF Orientation, nên phải xoay pixel trước khi bỏ metadata, nếu không ảnh hiện nằm ngang. GIF có thể là ảnh động nên không nên nén lại như ảnh tĩnh. Ảnh nhỏ sẵn có thể nặng hơn sau khi nén, vì vậy cần giữ bản gốc khi bản nén không nhỏ hơn.

    Điểm cộng: nhắc rằng bỏ metadata còn che được vị trí GPS trong ảnh điện thoại, và đặt giới hạn kích thước ở phía kho lưu trữ làm lớp bảo vệ thứ hai.

    Mở trang riêng của câu hỏi
  41. MiddleAIEmbedding là gì, và tìm kiếm bằng embedding khác tìm theo từ khóa ở điểm nào?

    Người phỏng vấn muốn biết bạn hiểu embedding như một công cụ cụ thể, có ưu và nhược, chứ không phải một từ khóa nghe cho oai. Họ cũng muốn xem bạn có biết cách kiểm tra xem nó có thật sự tốt hơn hay không.

    Embedding là cách biến một đoạn chữ thành một vector, tức một dãy số, bằng một mô hình đã được huấn luyện để hai đoạn gần nghĩa thì vector gần nhau. Để tìm kiếm, bạn nhúng sẵn các đoạn tài liệu và lưu vector lại. Khi có câu hỏi, bạn nhúng câu hỏi bằng cùng mô hình rồi lấy những đoạn có vector gần nhất, thường đo bằng cosine similarity.

    Khác biệt chính so với tìm theo từ khóa:

    1. Từ khóa so chữ, embedding so nghĩa. Khách viết "đền tiền", tài liệu viết "hoàn tiền": đếm từ thấy chữ quan trọng (đền với hoàn) khác nhau, còn embedding vẫn xếp đoạn đó cao.
    2. Điểm của embedding chỉ có ý nghĩa tương đối. Nhiều mô hình cho điểm dồn trong khoảng hẹp, nên bạn so thứ hạng chứ không đọc điểm như phần trăm.
    3. Từ khóa mạnh với mã, tên riêng, chuỗi chính xác. Tìm một mã sản phẩm hay một tên hàm thì khớp chữ chính xác lại đáng tin hơn so nghĩa.

    Ý quan trọng để nêu: đừng tin là embedding luôn tốt hơn. Cách làm đúng là có một bộ câu hỏi mẫu kèm đoạn tài liệu đúng cho từng câu, chạy cả hai cách và đếm câu nào được tìm đủ thông tin. Bộ câu hỏi cũng giúp bạn phát hiện những giả định sai, ví dụ nhúng thêm tiêu đề mục tưởng là có lợi nhưng đo ra lại kém hơn.

    Điểm cộng: nhắc rằng mô hình embedding có giới hạn độ dài đầu vào (nhiều mô hình cắt ở khoảng 512 token), nên đoạn tài liệu cần tách vừa đủ nhỏ, và nhắc rằng dùng đúng tiền tố mô hình yêu cầu (như query: và passage: ở họ e5) cũng ảnh hưởng chất lượng.

    Mở trang riêng của câu hỏi
  42. MiddleAI AgentContext window là gì? Chatbot "nhớ" cuộc trò chuyện bằng cách nào, và làm gì khi hội thoại quá dài?

    Context window là toàn bộ phần chữ mô hình đọc được trong một lần gọi: system prompt, định nghĩa công cụ, mọi tin nhắn gửi kèm và cả câu trả lời nó đang viết. Kích thước tính bằng token và mỗi mô hình có một giới hạn riêng.

    Ý người phỏng vấn muốn nghe đầu tiên là API của mô hình không có trạng thái. Nó không nhớ gì giữa hai lần gọi. Chatbot "nhớ" được là vì code của bạn giữ mảng tin nhắn và gửi lại toàn bộ ở mỗi lượt. Quên gửi thì tin nhắn thứ hai của khách đến với mô hình như câu đầu tiên của một người lạ.

    Hệ quả là lịch sử càng dài thì mỗi lần gọi càng tốn. Lần gọi thứ N gửi lại cả N−1 lượt trước, nên tổng số token của cả cuộc trò chuyện tăng nhanh hơn nhiều so với số tin nhắn. Với các mô hình có context window lớn, bạn thường gặp chi phí và độ trễ trước khi gặp giới hạn.

    Khi hội thoại dài, có ba cách thường dùng, và có thể kết hợp:

    • Cửa sổ trượt: chỉ gửi vài lượt gần nhất. Phải cắt ở ranh giới lượt, vì một kết quả công cụ (tool_result) phải đứng ngay sau lời gọi công cụ (tool_use) tương ứng. Cắt theo số tin nhắn rất dễ chẻ đôi một cặp và bị API từ chối.
    • Ghi nhớ có cấu trúc: code tự rút những dữ kiện quan trọng (mã đơn, tên khách) từ kết quả công cụ rồi đưa vào system prompt. Ít thông tin nhưng không sai.
    • Tóm tắt: nhờ mô hình tóm tắt phần cũ thành một đoạn ngắn. Nhớ được nhiều hơn nhưng có thể bỏ sót hoặc ghi sai chi tiết, nên không dùng cho những con số phải chính xác.

    Điểm cộng: nói rằng bạn sẽ đo số token đầu vào thật (trường usage trong phản hồi) trước khi tối ưu, lưu lịch sử đầy đủ ở database và chỉ cắt bản gửi đi, và biết phân biệt bộ nhớ trong một cuộc trò chuyện với bộ nhớ lâu dài giữa các phiên.

    Mở trang riêng của câu hỏi
  43. MiddleAI AgentMCP là gì, và nó khác tool calling ở điểm nào?

    Người phỏng vấn muốn biết bạn phân biệt được cơ chế mô hình yêu cầu chạy công cụ với cách đóng gói và chia sẻ công cụ giữa các ứng dụng.

    Tool calling là khả năng của mô hình: bạn mô tả công cụ (tên, mô tả, schema đầu vào) trong yêu cầu gửi cho mô hình, và nó trả về một khối cho biết muốn gọi công cụ nào với đầu vào gì. Chính ứng dụng của bạn mới chạy công cụ rồi gửi kết quả lại. Mô hình không tự chạy gì cả.

    MCP (Model Context Protocol) là một chuẩn mở để ứng dụng AI kết nối với công cụ và dữ liệu bên ngoài. Công cụ được viết một lần trong một MCP server, và mọi ứng dụng có MCP client đều liệt kê và gọi được nó. Hai thứ không loại trừ nhau: ứng dụng lấy danh sách công cụ từ MCP server, đổi sang định dạng mà API của mô hình nhận (tool calling), rồi khi mô hình yêu cầu thì chuyển lời gọi sang server. Nói gọn: tool calling là cách mô hình đòi chạy công cụ, còn MCP là cách công cụ được đóng gói để nhiều ứng dụng cùng dùng.

    Khi nào đáng dùng MCP: khi một bộ công cụ phục vụ nhiều hơn một ứng dụng, hoặc khi bạn muốn dùng server có sẵn của người khác. Nếu chỉ có một ứng dụng và công cụ ít đổi, một hàm thường trong cùng code đơn giản hơn, vì MCP thêm một tiến trình và một lớp giao thức.

    Về an toàn, đặc tả của MCP khuyến nghị luôn có con người có thể từ chối lời gọi công cụ, và coi các gợi ý như readOnlyHint là không đáng tin trừ khi server đáng tin. Server vẫn phải tự kiểm tra đầu vào và quyền, vì đầu vào là do mô hình viết ra.

    Điểm cộng: nhắc rằng với stdio, stdout dành cho giao thức nên log phải ra stderr, và phân biệt lỗi giao thức với lỗi khi chạy công cụ (trả về kết quả kèm isError).

    Mở trang riêng của câu hỏi
  44. MiddleAI AgentPrompt injection là gì, khác nhau thế nào giữa tấn công trực tiếp và gián tiếp, và bạn phòng chống ra sao?

    Người phỏng vấn muốn biết bạn có nhận ra mô hình không phân biệt được chỉ dẫn của bạn với văn bản người khác viết, và biết đặt biện pháp bảo vệ ở nơi đáng tin hơn là lời dặn trong prompt.

    Prompt injection là khi văn bản do người khác kiểm soát lọt vào ngữ cảnh và làm mô hình hành xử ngoài ý muốn. Tấn công trực tiếp là khi chính tin nhắn của người dùng làm việc đó, ví dụ "bỏ qua mọi hướng dẫn trước đó". Tấn công gián tiếp là khi nội dung nằm trong dữ liệu mà agent đọc, như một ô ghi chú, một trang web hay một tài liệu, và người dùng thậm chí không viết nó. OWASP xếp đây là rủi ro đầu tiên của ứng dụng dùng mô hình ngôn ngữ, và ghi rằng chưa rõ có cách ngăn chặn tuyệt đối.

    Vì vậy biện pháp nên đặt trong code: quyền tối thiểu (agent không có công cụ làm việc nhạy cảm như hoàn tiền, chỉ chuyển cho người), kiểm tra quyền trong từng công cụ bằng danh tính do ứng dụng xác thực chứ không do mô hình khai, chỉ đưa vào ngữ cảnh những trường cần thiết, lọc đầu ra, và cho người duyệt các việc rủi ro cao. Lời dặn trong system prompt và việc đánh dấu nội dung không đáng tin vẫn nên làm nhưng chỉ là lớp bổ sung.

    Cần nói thêm: bộ lọc đầu ra bằng biểu thức chính quy luôn có thể bị lách bằng cách diễn đạt khác, và có thể chặn nhầm câu đúng, nên phải thử cả câu đúng lẫn câu sai.

    Điểm cộng: nhắc việc biến các tấn công đã biết thành bộ test chạy lại được, và phân biệt rằng test với mô hình giả chỉ chứng minh code xung quanh, không chứng minh hành vi của mô hình thật.

    Mở trang riêng của câu hỏi
  45. MiddleAI AgentVòng lặp agent (agent loop) hoạt động thế nào? Làm sao để agent không chạy mãi?

    Vòng lặp agent là đoạn code bao quanh mô hình, cho phép nó làm nhiều bước liên tiếp thay vì trả lời một lần. Mỗi vòng gồm bốn việc:

    1. Gửi toàn bộ lịch sử messages cho mô hình, kèm danh sách công cụ.
    2. Thêm nguyên văn phản hồi của mô hình vào lịch sử.
    3. Nếu mô hình không xin công cụ (với Claude là stop_reason khác tool_use), đó là câu trả lời cuối và vòng lặp kết thúc.
    4. Nếu có, chạy mọi công cụ được xin, gom kết quả vào một tin nhắn, thêm vào lịch sử rồi lặp lại.

    Ý quan trọng là số bước do mô hình quyết định, không phải code. Ví dụ khách nhắn "tìm đơn theo số điện thoại, nếu chưa giao thì thêm một bạc xỉu": mô hình tự gọi lần lượt tìm đơn, xem trạng thái, thêm món, rồi mới trả lời. Code chỉ lặp.

    Vì vậy phần thứ hai của câu hỏi mới là phần người phỏng vấn muốn nghe. Nên nêu:

    • Giới hạn số lần gọi mô hình (ví dụ 6). Chạm giới hạn thì dừng, trả một câu trả lời an toàn và ghi log để người kiểm tra, thay vì ném lỗi hay lặp tiếp.
    • Luật nằm trong công cụ, không chỉ trong prompt. Hàm thêm món tự từ chối đơn đang giao, dù mô hình có gọi sai thứ tự.
    • Lỗi công cụ trả về cho mô hình (is_error) để nó đổi hướng, nhưng cần đề phòng mô hình thử lại mãi cùng một lời gọi.
    • Chi phí tăng theo số vòng: mỗi vòng gửi lại cả lịch sử, nên câu 4 bước tốn nhiều token hơn 4 lần câu 1 bước.

    Điểm cộng: nói thêm rằng nên có giới hạn thời gian hoặc ngân sách token bên cạnh giới hạn số vòng, và các SDK thường có sẵn tool runner chạy hộ vòng lặp này.

    Mở trang riêng của câu hỏi
  46. MiddleDatabaseIndex trong database là gì? Có nên đánh index cho mọi cột không?

    Index là một cấu trúc dữ liệu phụ (thường là B-tree) giúp tìm hàng theo giá trị một cột mà không phải quét toàn bảng, giống mục lục của một cuốn sách.

    Không nên đánh index mọi cột vì:

    • Mỗi lần INSERT, UPDATE, DELETE đều phải cập nhật thêm index, ghi chậm hơn.
    • Index tốn dung lượng.
    • Cột ít giá trị khác nhau (như status chỉ có 2 giá trị) thường không được lợi nhiều.
    Đánh index cho truy vấn thật sự hay chạy
    create index posts_published_at_idx on posts (published_at desc)
      where status = 'published';

    Câu trả lời tốt sẽ nhắc tới việc đo trước bằng EXPLAIN ANALYZE rồi mới thêm index cho đúng truy vấn chậm.

    Mở trang riêng của câu hỏi
  47. MiddleDatabaseRow Level Security (RLS) trong PostgreSQL là gì? Nó khác gì với GRANT?

    PostgreSQL có hai lớp kiểm soát quyền trên một bảng, và câu trả lời tốt phân biệt được chúng.

    GRANT quyết định một vai được làm thao tác gì trên bảng: select, insert, update, delete. Nó không phân biệt các dòng, đã cho select là đọc được mọi dòng.

    Row Level Security quyết định vai đó được đụng tới những dòng nào. Sau khi chạy alter table ... enable row level security, mỗi thao tác phải thỏa một policy, tức một điều kiện SQL kiểm tra trên từng dòng. Ví dụ cho blog:

    sql
    create policy "Ai cũng đọc được bài đã đăng" on posts for select
    to anon using (status = 'published' and published_at <= now());

    Vài ý nên nêu để cho thấy bạn đã dùng thật:

    • Bật RLS mà không có policy thì mặc định từ chối tất cả. Câu select không báo lỗi mà trả về 0 dòng, nên trang trống dù bảng có dữ liệu. Đây là lỗi rất hay gặp.
    • Thao tác ghi bị chặn im lặng: update và delete trên dòng không được thấy sẽ ảnh hưởng 0 dòng, còn insert vi phạm policy thì báo lỗi.
    • Chủ bảng, superuser và vai có BYPASSRLS bỏ qua RLS. Vì vậy thử policy phải dùng đúng vai của ứng dụng (ví dụ set role anon), và khóa quản trị không bao giờ được nằm ở trình duyệt.
    • Vì sao cần RLS khi đã lọc trong code: với các nền tảng cho trình duyệt gọi thẳng database qua API (như Supabase), khóa nằm ở phía client nên ai cũng gọi được. Bộ lọc trong code không bảo vệ được gì, policy trong database mới là thứ chặn.

    Điểm cộng: nói rằng nên dùng cả hai lớp, chỉ grant đúng thao tác cần thiết, để một lần cấp nhầm quyền không làm lộ hay mất dữ liệu.

    Mở trang riêng của câu hỏi
  48. MiddleLàm việc nhómBạn xử lý thế nào khi không đồng ý với góp ý trong code review?

    Người phỏng vấn muốn biết bạn có tách được cái tôi khỏi code hay không.

    1. Hỏi lại để hiểu đúng lý do của reviewer trước khi phản hồi.
    2. Trả lời bằng dữ kiện: đo đạc, tài liệu, quy ước của team, không bằng cảm giác.
    3. Nếu vẫn khác ý và vấn đề nhỏ, chọn cách của reviewer hoặc ghi lại để bàn sau, đừng chặn PR.
    4. Nếu vấn đề lớn, chuyển sang trao đổi trực tiếp thay vì tranh luận dài trong comment.

    Kể một ví dụ thật, ngắn gọn, có kết quả cụ thể sẽ thuyết phục hơn liệt kê nguyên tắc.

    Mở trang riêng của câu hỏi
  49. MiddleNode.jsMột API Node.js trả lời chậm, bạn tìm nguyên nhân thế nào trước khi sửa?

    Người phỏng vấn muốn biết bạn có làm việc dựa trên số đo thay vì đoán, và có hiểu vì sao một đoạn code chạy nhanh với dữ liệu nhỏ lại chậm khi dữ liệu lớn.

    Bước đầu tiên là biết chậm ở đâu ở mức request. Nếu máy chủ có log ghi thời gian xử lý mỗi request, bạn lọc ra đường nào chậm. Nếu chưa có, hãy đo bằng performance.now() hoặc thêm log đó trước. Bước thứ hai là kiểm tra giả thuyết bằng cách đo riêng từng phần nghi ngờ (định dạng dữ liệu, chuyển JSON, truy vấn cơ sở dữ liệu): nếu một phần chỉ chiếm vài mili giây trên tổng vài giây thì đừng tối ưu nó. Bước thứ ba là dùng profiler, ví dụ cờ --cpu-prof của Node.js ghi file .cpuprofile, để xem hàm nào tốn nhiều thời gian riêng nhất.

    Một nguyên nhân hay gặp là độ phức tạp tăng theo bình phương: dùng find hoặc includes trên một mảng ngay trong vòng lặp. Với vài trăm phần tử thì không thấy, với hàng chục nghìn thì chậm hẳn. Cách sửa thường là dùng Map hoặc Set để tìm theo khóa.

    Sau khi sửa, kiểm chứng hai lần: kết quả có còn đúng không (giữ bản cũ để so, hoặc viết test) và nhanh hơn bao nhiêu (đo nhiều lần, lấy trung vị, ở nhiều cỡ dữ liệu để thấy hình dạng của đường tăng).

    Điểm cộng: nhắc rằng không phải đoạn code nào cũng đáng tối ưu, bản dễ đọc ở quy mô nhỏ là quyết định hợp lý, và số đo chỉ đúng với máy và dữ liệu bạn đo.

    Mở trang riêng của câu hỏi
  50. MiddleReactuseEffect trong React chạy khi nào? Mảng phụ thuộc dùng để làm gì?

    useEffect chạy sau khi React đã cập nhật giao diện. Nó dùng để đồng bộ component với thứ bên ngoài React: gọi API, đăng ký sự kiện, hẹn giờ.

    • Không có mảng phụ thuộc: chạy sau mỗi lần render.
    • Mảng rỗng []: chạy một lần sau lần render đầu tiên.
    • Có giá trị [id]: chạy lại khi id thay đổi.

    Hàm trả về từ effect là hàm dọn dẹp, chạy trước lần effect tiếp theo và khi component bị gỡ, ví dụ để hủy hẹn giờ hay gỡ sự kiện.

    Mở trang riêng của câu hỏi
  51. MiddleWebXSS là gì, và làm sao hiển thị nội dung Markdown do người dùng nhập mà không bị XSS?

    Người phỏng vấn muốn biết bạn có hiểu XSS là chuyện nội dung không tin cậy được trình duyệt chạy như mã, và bạn có biết nó lọt vào từ những chỗ nào, không chỉ thuộc lòng tên.

    XSS (cross-site scripting) xảy ra khi dữ liệu của người dùng được đưa vào trang theo cách trình duyệt hiểu là HTML hoặc mã thay vì là chữ. Khi đó kẻ tấn công chạy được mã trong trang của bạn, với quyền của người đang xem: đọc cookie, gọi API thay họ, đổi nội dung trang.

    Với Markdown, rủi ro chính đến từ ba chỗ:

    1. HTML thô. Markdown cho phép viết HTML nên các thư viện chuyển nó sang đầu ra nguyên văn, kể cả thẻ như <img onerror=...>. Thẻ <script> chèn qua innerHTML không chạy, nhưng nhiều cách khác vẫn chạy.
    2. Liên kết và ảnh có giao thức nguy hiểm. [xem](javascript:...) chạy mã khi bấm.
    3. Bộ lọc cấu hình sai. Danh sách cho phép quá rộng, hoặc lọc bằng regex tự viết, rất dễ bị lách.

    Có hai hướng xử lý đáng nhắc tới. Hướng một là chuyển Markdown sang HTML rồi lọc bằng thư viện sanitize chuyên dụng theo danh sách cho phép (DOMPurify, sanitize-html), chỉ giữ những thẻ và thuộc tính cần. Hướng hai là không tạo ra chuỗi HTML: phân tích Markdown thành các khối có kiểu rồi dựng bằng framework như React, nơi chữ được đặt làm nội dung phần tử nên tự được thoát ký tự. Với hướng hai, bạn vẫn phải tự kiểm tra địa chỉ của liên kết và ảnh, chỉ nhận http, https và đường dẫn trong site.

    Ý quan trọng để nêu: đừng dùng dangerouslySetInnerHTML (hoặc innerHTML) cho nội dung chưa được lọc, và mọi đường đưa nội dung ra trang, kể cả ô xem trước trong trang quản trị, phải đi qua cùng một bộ xử lý an toàn.

    Điểm cộng: nhắc tới Content Security Policy như lớp bảo vệ thứ hai, hạn chế nơi mã được phép chạy, và nhắc rằng một điều kiện an toàn thì nên có test, ví dụ test cho các địa chỉ javascript: và data:.

    Mở trang riêng của câu hỏi
  52. SeniorKiến trúcBạn sẽ thiết kế giới hạn tốc độ (rate limiting) cho một API công khai thế nào?

    Bắt đầu bằng câu hỏi làm rõ: giới hạn theo ai (IP, API key, người dùng), mức nào, chặn cứng hay giảm tốc, hệ thống chạy một hay nhiều máy chủ.

    Các thuật toán thường gặp:

    • Fixed window: đếm theo khung phút, đơn giản nhưng dồn request ở mép khung.
    • Sliding window: mượt hơn, tốn bộ nhớ hơn.
    • Token bucket: cho phép bùng nổ ngắn, phổ biến nhất cho API.

    Với nhiều máy chủ, bộ đếm cần nằm ở chỗ dùng chung như Redis với thao tác nguyên tử. Trả về mã 429 Too Many Requests kèm header Retry-After, và ghi log để phát hiện lạm dụng.

    Ở mức senior, nói thêm về đánh đổi: độ chính xác so với chi phí, xử lý khi Redis lỗi (cho qua hay chặn hết), và giới hạn khác nhau cho từng gói khách hàng.

    Mở trang riêng của câu hỏi
  53. SeniorKiến trúcKhi bắt đầu một dự án mới, bạn chọn công nghệ dựa trên những tiêu chí nào?

    Bắt đầu từ bài toán và ràng buộc, không bắt đầu từ công nghệ đang hot.

    • Ai sẽ vận hành và bảo trì sau 6 tháng, team đã quen gì.
    • Ràng buộc thật: ngân sách, hạn chót, SEO, yêu cầu offline, quy mô người dùng.
    • Chi phí chuyển đổi nếu chọn sai, và có lối thoát hay không.
    • Hệ sinh thái, tài liệu, khả năng tuyển người.

    So sánh tối đa ba lựa chọn theo đúng các tiêu chí đó và ghi lại quyết định (một tài liệu ADR ngắn) để lần sau xem lại có còn đúng không.

    Mở trang riêng của câu hỏi