23 questions
Interview questions
Practise answering at the level you are applying for. Each question has a short answer and what the interviewer really wants to hear.
- Junior 0–2 years: fundamentals, everyday tools, working safely in a team.
- Middle 2–5 years: owning a feature end to end, understanding trade-offs.
- Senior 5+ years: system design, technical leadership, long-term decisions.
How is an AI agent different from a chatbot and a workflow? When should you not use an agent?
Open this question on its own pageA good answer tells the three apart with one question: who decides the next step?
- Rule-based chatbot: hand-written rules decide (keyword matching, for example). Cheap, but it doesn't understand sentences; "has my order been delivered?" can be read as a question about the delivery fee because it contains "deliver".
- Workflow: a developer writes the path in code in advance; the LLM only does small steps such as classifying or writing the reply. Predictable, easy to test and cheap, but it can only follow the paths that were written.
- Agent: the LLM chooses the next step itself, in a loop: call a tool, read the result, decide whether to call another tool or answer. It can handle multi-step, conditional requests such as "if my order hasn't gone out yet, add one more item".
Don't use an agent when:
- the kinds of request are known up front and each has a clear way to handle it (a workflow is enough, faster and cheaper);
- a single LLM call with the right information already solves it;
- the action is high-risk (money, deleting data) and no person reviews it.
Bonus points: mention a limit on the number of loop steps, the fact that every step costs a model call, and a combined design, with a workflow at the front door and the agent only for complex requests.
What is LLM hallucination? When you build a customer service chatbot, how do you reduce it?
Open this question on its own pageHallucination is when a language model produces information that sounds plausible but is wrong, or isn't in the data it was given. For example, a customer asks "has order 1041 been delivered?", the request sent to the model contains no order data at all, and the model still answers "yes, your order has been delivered!".
The key point to get across is that the model only knows what is in the request plus the general knowledge it learned in training. It doesn't connect to your database by itself. So for data that keeps changing, such as an order's status, writing a better prompt is not the fix.
Ways to reduce it, from easiest to most reliable:
- Put the information it needs into the prompt (opening hours, prices) and tell it to "only use this information".
- Allow the model to say "I don't know", along with a concrete replacement answer. Anthropic's documentation lists this as the first basic technique.
- For dynamic data, let your code look up the real data and hand the result to the model, using tool calling or RAG.
- Add a code check for risky answers, with tests for that check.
Bonus points: say clearly that these techniques reduce hallucinations rather than eliminate them, so high-risk actions (money, promises to customers) still need a person to approve them.
What is tool calling (function calling)? When you use it, does the model run your functions itself?
Open this question on its own pageTool calling is how an LLM gets to use data and actions outside itself. The most important point to make is that the model never runs any function itself: it only writes a request, and your code decides whether to run it.
One tool call goes through four steps:
- The application sends a request with a list of tools. Each tool has a name, a description (including when it should be used) and a JSON Schema for its input.
- If it needs a tool, the model returns a
tool_useblock (the tool name and arguments) withstop_reasonset totool_use, instead of an answer. - The application runs the real function, for example reading order 1041 from the database, and sends the result back in a
tool_resultblock with the sameid. - The model reads the result and writes the final answer.
It's worth adding a few points that show you've actually built one:
- Input written by the model is untrusted data, and you check it the way you'd check data a user submits (format, permissions). The schema only guarantees the types, not that the values make sense.
- Tool errors should go back to the model (for example
is_error: truewith "there is no order 1099") so it can ask the customer again, instead of crashing the whole application. - Every tool use costs an extra API call, and the tool definitions count as input tokens in every request.
- When a question needs several steps (find the order number by phone number, then look up its status), the application has to keep looping until the model answers in text. That's the agent loop.
How do you choose an AI coding tool (Copilot, Cursor, Claude Code…)? What criteria do you use?
Open this question on its own pageA good answer doesn't rank the tools. It shows that you understand how they differ and how you stay in control of the result.
The first criterion is how much work you hand over. Suggestions as you type (Copilot, Cursor's Tab) keep you writing every line. An agent inside the editor edits several files and lets you review each change. An agent that runs commands, like Claude Code, runs the tests, reads the errors and keeps fixing. The more you hand over, the more carefully you have to review.
The second criterion is your company's rules: which tools are allowed, whether code may leave the company, and what data policies apply. At a company, this is usually the deciding factor.
The third criterion is cost and free plans; prices change constantly, so always check the official pricing page.
The most important part is how you check the result, whichever tool you use:
- write a short spec and tests before handing the task over;
- say explicitly "don't change the tests";
- run the tests yourself under the same conditions as production (for example, the UTC time zone);
- try a few cases beyond the tests, and be able to explain every part of the code before merging.
Bonus points: describe a specific bug that your tests, or your own extra checks, caught.
How do you use AI tools when you code? How do you know the code the AI wrote is correct?
Open this question on its own pageThe interviewer isn't asking whether you use AI; they want to know whether you stay in control of the code you submit. A good answer describes a concrete process:
- Define "correct" before asking the AI. Write a short requirement with a few example inputs and expected results, including the error cases. For example: the message "coffee 35k" must give 35,000 đồng, and a message with no amount must be rejected.
- Turn the examples into tests, let the AI write code until the tests pass, and run the tests again yourself.
- Read and be able to explain every part of the code before committing. Anything you can't explain, ask about again or rewrite.
- Try cases beyond the examples. Tests only protect what you thought of; "35 thousand" might be read as 35 đồng while every test stays green.
- Check APIs and libraries against the official documentation, because AI can call functions that don't exist or use outdated syntax.
- Never paste secrets (API keys, customer data, company code you aren't allowed to share) into an AI tool.
Bonus points: tell a specific story of when you caught AI-written code being wrong, and how you caught it.
When you ask AI to write or fix code, how do you write the prompt so the result is usable?
Open this question on its own pageThe interviewer isn't looking for a magic phrase. They want to know whether you stay in control or just paste a request and accept whatever code comes back. A good answer names three things every prompt should make clear:
- What to do, with enough context: which files, which spec, the conditions it runs under (for example, tests run with
TZ=UTC). - What not to do: "don't write code yet, ask about anything unclear", "don't change the tests", "don't add libraries", "fix only issue number 1". Without constraints, AI tends to take the shortest path, like editing a test until it's green.
- How the result will be checked: "run the tests and paste the output", "the new tests must fail on the current code", "for every claim, give me a command I can run to verify it".
Tell a concrete example. For instance: before changing a command in an app, you asked AI to list the inputs most likely to go wrong, ran them yourself, and found that the message "/total month 10" was recorded as an expense of 10 đồng (Vietnam's currency). Then you wrote failing tests first, and only then asked AI to fix the code without touching the tests.
Bonus points: say that the checking step is the part you don't hand over to AI. An explanation that sounds reasonable isn't necessarily right, so you run the commands, measure, read the diff, and only then accept it.
- What to do, with enough context: which files, which spec, the conditions it runs under (for example, tests run with
What is a database migration? What should you watch out for when adding a NOT NULL column to a table that already has data?
Open this question on its own pageA migration is a file (usually SQL) that describes one change to the schema, such as creating a table, adding a column or adding an index. The files are named in time order, committed to Git and run one after another, so the databases on dev machines, staging and production all go through exactly the same steps. It also means schema changes get reviewed in pull requests, just like code.
For the NOT NULL part, the interviewer wants to hear two things.
First,
alter table posts add column tags text[] not nullfails on a table that has data: the existing rows have no value for the new column, which meansnull, and that violates the constraint (PostgreSQL reportscolumn "tags" of relation "posts" contains null values). On a dev machine with an empty table it runs fine, so the error only shows up in production. The fix is a default value:not null default '{}'. If there's no sensible default, split it into steps: add the column as nullable, fill in the data, then setnot null.Second, backward compatibility. During a deploy, the migration and the new code don't switch over at the same instant. There's usually a window where the old code runs against the new schema, so a migration must not break the old version: don't drop or rename columns the old code still uses, and give new columns a default so the old
insertstatements keep working.Bonus points: say that a migration that has already run in production is never edited; if you need to change something, you write a new migration.
Why don't we usually store images directly in the database? Where do you store them?
Open this question on its own pageImages are large and you almost never query them by their content. Storing them directly in the database (as binary or base64) causes several problems:
- It uses the most expensive space. Database storage is usually limited and costs more than file storage. For example, Supabase's free plan has a 500 MB database: a long post is only a few dozen KB of text, while a single phone photo is several MB. A few dozen posts with images and it's full.
- It's slow and heavy. Every query drags large data along, backups and restores take longer, and the database's cache fills up with image bytes.
- It can't use a CDN. Images in object storage are served through a CDN and cached close to users; images in the database have to go through the application server.
The common approach: store image files in object storage (Supabase Storage, Amazon S3, Cloudflare R2…) and keep only the path and some metadata (size, alt text, who uploaded it) in the database. On top of that, compress images before uploading (resize, convert to WebP/AVIF) to cut both storage and bandwidth.
Bonus points: mention cleaning up images that no post uses anymore, and permissions for the file store, meaning who can upload and who can view.
What is the difference between git merge and git rebase? When should you use each?
Open this question on its own pageBoth bring changes from one branch into another, but they record history in two different ways.
git mergecreates a merge commit that joins the two branches. History stays as it happened, and you can see exactly when the branch split off and came back.git rebaserewrites your branch's commits on top of the target branch. History becomes a straight line that is easier to read, but the commits get new hashes.
git switch feat/login git fetch origin git rebase origin/main # straight history; only do this on a branch that is yours aloneIn an interview, mentioning the trade-off (true history versus readable history) and the rule your team actually follows earns more points than the definitions alone.
What's the difference between GET and POST? Why shouldn't you use GET to delete data?
Open this question on its own pageGET is for reading data: its parameters live in the URL, and it can be cached, saved in history and called again automatically by browsers or scanning tools (prefetching, link previews). POST is for sending data that changes state, and the data lives in the request body.
GET must be safe (it changes nothing) and idempotent (calling it many times has the same effect as calling it once). If a GET link deletes data, a single bot scanning links or a browser prefetching is enough to lose that data.
How is a breakpoint different from console.log when debugging? When do you still use console.log?
Open this question on its own pageA breakpoint pauses the program on one line and lets you see the value and type of every variable, see who called the function (the call stack), and type more expressions that are evaluated right in that context.
console.logonly prints what you guessed in advance you'd need.One example shows the difference: a menu price was stored by mistake as the string
"35.000"(thirty-five thousand, written with a dot between the thousands as in Vietnam and much of Europe).console.log(item.price)prints35.000, which looks like a perfectly normal price. In the debugger's Variables panel the value shows up with quotes, andtypeofreturns'string'. The bug is obvious at once:"35.000" * 2is 70, because JavaScript reads the dot as a decimal point.Tools worth mentioning: conditional breakpoints (only stop when
order.id === 1044), Watch, the Debug Console, the Call Stack, and logpoints (log without pausing and without editing the code).console.logstill makes sense:- in a small script you run once;
- for timing bugs, where pausing the program changes the order things run in;
- in production, where you can't attach a debugger, and there you should write structured logs.
Bonus points: logging
JSON.stringify(value)or{ value }makes it clear whether you have a string or a number.What is a stack trace? When you see "Cannot read properties of undefined", how do you find the cause?
Open this question on its own pageA stack trace is the list of function calls that were still in progress when an error happened, with the file name, line and column of each call. In JavaScript, the top frame is where the error happened; the frames below it are the path that led there.
For
TypeError: Cannot read properties of undefined (reading 'name'), a good answer goes through these steps:- Read the message: the property being read is
name, so whatever comes right before.nameisundefined. - Find the first frame from your own code: skip frames in
node_modulesornode:internal, then open that exact file, line and column. - Trace the data back: the line that threw is where the error showed up, not necessarily where it started. Walk down the frames to see where the
undefinedvalue came from: an API that hasn't returned yet, empty initial state, or data that is missing a field. - Reproduce it with the smallest possible example, then fix it in the right place: fix the data at its source, or handle a legitimate missing value with a default or
?..
Bonus points if you add that when you catch an error and throw a new one,
new Error("…", { cause: err })keeps the original error, and that minified production code needs source maps before its stack traces are readable.- Read the message: the property being read is
What's the difference between var, let and const in JavaScript?
Open this question on its own pagevaris function-scoped, is hoisted (the declaration is moved to the top of the function with the valueundefined) and can be declared again.letis block-scoped{ }, can't be declared again in the same block, and can't be used before the line that declares it.constis likeletbut can't be reassigned. Note: an object or array declared withconstcan still be changed inside.
const user = { name: "Alex" }; user.name = "Sam"; // fine user = {}; // TypeErrorA good habit: use
constby default, useletonly when you really need to reassign, and avoidvarin new code.In the Next.js App Router, how is a route group (folder) different from a private _folder? When do you use each?
Open this question on its own pageBoth are folder naming conventions inside
app, but they solve two different problems.A route group is a folder whose name is in parentheses, like
(site)or(shop). Next.js uses it to group pages but leaves the group name out of the URL:app/(site)/posts/page.tsxis still/posts. Its main use is giving a set of pages their own shared layout. For example, a blog's public pages need a header and footer and are generated ahead of time, while the admin pages need their own frame and are built fresh on every visit. Put the public pages in a(site)group with its own layout and the two sides stay separate without the URLs changing.A private folder has a name starting with an underscore, like
_components. Next.js ignores that folder and all its subfolders when building routes, so it's a place to keep non-page code (components, helpers) right next to a page.A common follow-up: even without the underscore, a file in
appdoesn't become a page unless it's apageorroutefile. So_folderis mainly about making the intent clear and avoiding clashes with Next.js's special file names.Bonus points: mention that two layouts can apply to the same
/(the root layout and the group's layout) while wrapping different scopes.If you could rebuild your personal project from scratch, what would you change?
Open this question on its own pageThis question checks whether you look back at your own work. "I think it's fine, I wouldn't change anything" is the weakest answer, because it suggests you learned nothing from the project.
A good answer usually has three parts:
- One specific decision you would change, with the reason. For example: "I'd write the table of examples and the tests before writing the function that reads amounts. The first time, I wrote the code first, and the message 'train ticket 32K' was read as 32 billion đồng."
- Something you would keep, to show you can tell good decisions from bad ones. For example: "I'd still reject a message that contains two amounts, because recording the wrong amount is worse than asking the user to send it again."
- A lesson that carries over to other work, such as cutting scope early, thinking about the server's time zone, or asking real users for feedback sooner.
Avoid two traps: criticising the technology you chose without being able to say why, and describing a change so big that it sounds like throwing everything away and starting over.
Bonus: prepare this answer in advance for the first project on your resume, because interviewers often ask it right after "tell me about your project".
Tell me about the project on your resume you're most proud of. Which part did you build yourself?
Open this question on its own pageThe interviewer asks this to check three things: whether you really built the project, how deeply you understand it, and whether you can explain a piece of technical work so someone else understands it.
A good answer takes about 2 minutes, in this order:
- The problem: what the project does, and for whom. One sentence is enough.
- Your role: for a team project, say clearly which part was yours, for example "I built all of the login and permissions". Don't say "we did" for everything.
- One technical decision: which approach you chose, why, and what alternatives you considered. For example: "I stored images in file storage and kept only their paths in the database, so we wouldn't go over the database size limit."
- One difficulty and how you got past it: a specific bug, and how you found its cause.
- If you did it again: one thing you would do differently. This shows you can look back at your own work.
Avoid:
- Reciting a feature list as if you were reading the README.
- Claiming a part you can't answer the next question about. Interviewers often dig two or three levels deeper into exactly the part you claim.
Prepare ahead: for every project on your resume, write down the 5 points above and practise saying them out loud at least once.
Tell me about yourself in about one minute.
Open this question on its own pageThis question often opens an interview, and the person asking doesn't need to hear your whole resume again. They want to see what you choose to talk about in one minute, and whether you can say it clearly. A good answer usually has four parts, one or two sentences each:
- Who you are and what you do: "I'm a recently graduated frontend developer, working mainly with React and TypeScript."
- One concrete piece of evidence: a project, with its hardest part and the result. For example: "I built an app that records spending from text messages. The hard part was reading amounts the way people in Vietnam write them, like 35k or 1tr2 (1.2 million). I solved it with a table of examples where every row is a test. The app has 21 tests and a few classmates use it every day."
- What you're learning or want to get better at, stated specifically: "I'm learning to write tests for React components."
- Why you're here: connect it to the role, for example the company uses React and TypeScript, or its product has a lot of mobile users.
Things to avoid:
- Reading out your resume in chronological order, starting from high school.
- Adjectives nobody can check, like "dynamic, eager to learn, works well under pressure".
- Mentioning a project you can't answer the next question about, because interviewers often dig straight into the project you just brought up.
Prepare beforehand: write it down, then practise saying it out loud a few times with a timer to keep it to about a minute. Keep it consistent with your LinkedIn headline and About section, because the interviewer may have just read them.
What is a unit test? How do you know a test you wrote actually works?
Open this question on its own pageA unit test automatically checks a small piece of code (a function, a module): given this input, the result must be that. It runs fast and needs no server or database, so you can run it every time you save a file and before every merge.
The second half of the question is the part the interviewer really wants to hear. A test works when it turns red when the code is wrong. A few signs worth mentioning:
- The expected result comes from outside the code. Write
expect(orderTotal(order)).toBe(70000)because you know 2 coffees × 35,000 is 70,000. Don't recompute the expected value with the same formula you're testing, because then the test compares the code with itself and is always green. - Break the code on purpose. After fixing a bug, temporarily remove the fix and run the test again. If it's still green, it isn't checking the right thing.
- Include the cases that must be rejected, not only the ones that work. For example, a price parser should throw on
"35,000"instead of guessing. - The test name tells you what it protects, like "2 salt coffees at 35,000 cost 70,000", so whoever turns it red understands straight away without asking.
Bonus points: say that tests for each small function aren't enough. You also want a test that goes through the real data path (read raw data → normalize → total), because bugs often hide where a function is correct but no longer gets called.
- The expected result comes from outside the code. Write
What is an index in a database? Should you index every column?
Open this question on its own pageAn index is an extra data structure (usually a B-tree) that finds rows by a column's value without scanning the whole table, like the index at the back of a book.
You shouldn't index every column, because:
- Every
INSERT,UPDATEandDELETEalso has to update the indexes, so writes get slower. - Indexes take up storage.
- Columns with few distinct values (such as a
statuswith only 2 values) usually don't gain much.
create index posts_published_at_idx on posts (published_at desc) where status = 'published';A good answer mentions measuring first with
EXPLAIN ANALYZE, then adding an index for the specific slow query.- Every
When does useEffect run in React? What is the dependency array for?
Open this question on its own pageuseEffectruns after React has updated the screen. It's for keeping a component in sync with something outside React: calling an API, subscribing to events, setting timers.- No dependency array: it runs after every render.
- An empty array
[]: it runs once, after the first render. - With values such as
[id]: it runs again wheneveridchanges.
The function returned from the effect is the cleanup function. It runs before the next effect and when the component is removed, for example to clear a timer or remove an event listener.
How do you handle it when you disagree with feedback in a code review?
Open this question on its own pageThe interviewer wants to know whether you can separate your ego from your code.
- Ask questions to understand the reviewer's reasons properly before you reply.
- Answer with evidence: measurements, documentation, the team's conventions, not gut feeling.
- If you still disagree and the issue is small, go with the reviewer's way or note it to discuss later; don't block the PR.
- If the issue is big, move to a direct conversation instead of a long argument in the comments.
One real example, told briefly and with a concrete outcome, is more convincing than a list of principles.
How would you design rate limiting for a public API?
Open this question on its own pageStart with clarifying questions: limit by whom (IP, API key, user), at what level, hard block or slow down, and does the system run on one server or many.
Common algorithms:
- Fixed window: count per one-minute window; simple, but requests pile up at the window edges.
- Sliding window: smoother, uses more memory.
- Token bucket: allows short bursts; the most common choice for APIs.
With several servers, the counter has to live somewhere shared, such as Redis with atomic operations. Return
429 Too Many Requestswith aRetry-Afterheader, and log it so you can spot abuse.At senior level, also talk about the trade-offs: accuracy versus cost, what to do when Redis fails (let everything through or block everything), and different limits for each customer plan.
When you start a new project, what criteria do you use to choose the technology?
Open this question on its own pageStart from the problem and its constraints, not from whatever technology is trending.
- Who will run and maintain it six months from now, and what the team already knows.
- The real constraints: budget, deadline, SEO, offline requirements, number of users.
- The cost of switching if you choose wrong, and whether there's a way out.
- The ecosystem, the documentation, and how easy it is to hire people who know it.
Compare three options at most against exactly those criteria and write the decision down (a short ADR, architecture decision record) so you can check later whether it still holds.