MERN Stack · Interview Prep

Express.jsinterview questions & answers

163+ real Express.jsinterview questions with model answers, plus free lessons to learn the concepts. Prepare in English & Hinglish, then practise with an AI mock interview.

19 topics · 163+ questions

Lessons available in both languages

What you’ll learn

  • What is Express?
  • Express vs raw http
  • RoutingFree account
  • Middleware (the core)Free account
  • Request & ResponseFree account
  • Body parsingFree account
  • Route params & queryFree account
  • Router (modular routes)Free account
  • Error-handling middlewareFree account
  • Serving static filesFree account
  • CORSFree account
  • JWT auth middlewareFree account
  • Request validationFree account
  • REST API designFree account
  • HTTP status codesFree account
  • Interview recapFree account
  • Project 1: CRUD REST API
  • Project 2: JWT auth APIFree account
  • Project 3: Middleware pipelineFree account

What is Express?

Imagine you want to open a tea stall. You could build everything from scratch — chop your own wood, forge your own stove, dig a well for water. It works, but you'd spend all day on plumbing instead of making chai. Or you rent a ready-made stall kit: stove, counter, water tap, a menu board already set up. Now you just focus on serving customers. Express is that ready-made kit.

In Express, the raw work is Node's http module — remember the Node project-http-server, where you wrote if (req.url === '/health' && req.method === 'GET') by hand, set headers yourself, and JSON.stringify-ed every response? That's the "forge your own stove" version. Express is a small web framework that sits on top of that same http server and hands you the conveniences: clean routing (app.get('/users', ...)), request/response helpers (res.json(...)), and middleware — so you write far less boilerplate. You start with const app = express(), register routes like app.get(path, handler), and open the door with app.listen(port). Under the hood Express is still calling http.createServer — it doesn't replace Node, it wraps it.

🌍 Real-world example: Raw http needed ~8 lines just to answer GET /health with JSON — manual writeHead, manual Content-Type, manual stringify. In Express it's one line: app.get('/health', (req, res) => res.json({ status: 'ok' })). res.json sets the JSON header AND stringifies for you. Multiply that saving across 30 routes and you see why every MERN backend uses Express.

💡 web framework = a library that gives you a ready structure (routing, middleware, helpers) for building servers, so you write app logic instead of low-level plumbing.

💡 unopinionated = Express doesn't force a folder layout or database on you — you arrange the app however you like (the opposite of an opinionated framework).

💡 middleware = a function that runs on each request between arrival and response — used for logging, auth, body parsing, etc.

Where does it fit? Express is the "E" in MERN (MongoDB, Express, React, Node). React is the frontend the user sees; Express is the API / backend layer — it receives requests from React, talks to the database, and sends JSON back. Node is the runtime it all runs on. So when an interviewer asks "what is Express and why not just use http?", the answer is: it's a minimal framework over Node's http that adds routing, middleware, and req/res helpers so you build APIs faster with less boilerplate.

Standard definition: Express is a minimal, unopinionated web framework for Node.js built on top of the built-in http module, adding routing, middleware, and request/response helpers so you can build servers and APIs with far less boilerplate than raw http.

const express = require('express');
const app = express();

app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.listen(3000, () => console.log('Server on 3000'));

Express vs raw http

Imagine you need a car. Option one: buy raw parts — engine, wheels, chassis, wiring — and bolt them together yourself. It works, but you write every nut and bolt by hand and it's easy to forget something. Option two: buy a ready-made car where steering, brakes, and dashboard are already wired up — you just drive. That's the difference between Node's raw http module and Express.

In Express, you still get an HTTP server — but the boilerplate is done for you. Where raw http gives you one big handler and you branch on req.url/req.method by hand, Express gives you real routing (app.get('/users', ...), app.post(...)), req/res helpers (res.json(obj) sets the JSON header AND stringifies in one call — no manual writeHead + JSON.stringify), middleware (reusable functions in the request pipeline), and built-in body parsing (express.json() fills req.body). Same server, a fraction of the code.

🌍 Real-world example: A GET /health that returns {"status":"ok"}. In raw http you check req.url === '/health', res.writeHead(200, {'Content-Type':'application/json'}), then res.end(JSON.stringify({status:'ok'})) — three deliberate steps. In Express it's one line: app.get('/health', (req, res) => res.json({ status: 'ok' })). Add a second route in http and you're stacking more if/else; in Express you just add another app.get.

💡 routing = matching an incoming request's URL + HTTP method to the right handler function.

💡 middleware = a function (req, res, next) that runs during the request→response cycle (logging, auth, parsing) before your handler.

💡 body parsing = reading the raw bytes of a request body and turning them into a usable req.body object.

The key point — Express IS Node's http underneath, not a replacement. app.listen(3000) internally calls http.createServer(app) and hands your app to the same http server. req and res are the very same objects the http module gives you, just decorated with extra helpers like res.json and req.params. So Express isn't a new server or a new language — it's a thin, convenient layer over the exact http module you already learned. Knowing the raw version means you know what Express is doing for you.

Standard definition: Express is a minimal, unopinionated web framework that runs on top of Node's built-in http module, adding routing, middleware, and request/response helpers (like res.json and req.body) so you write far less boilerplate than raw http.createServer — but it still uses Node's http server underneath, not a replacement for it.

// ---------- RAW HTTP (the hard way) ----------
const http = require('http');
const users = [{ id: 1, name: 'Asha' }];

http.createServer((req, res) => {
  if (req.url === '/users' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(users));      // must stringify + set header by hand
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
}).listen(3000, () => console.log('Server on 3000'));

// ---------- EXPRESS (same server, less code) ----------
const express = require('express');
const app = express();
app.use(express.json());                 // built-in body parsing -> req.body

app.get('/users', (req, res) => res.json(users));            // routing + res.json
app.post('/users', (req, res) => {
  const user = { id: users.length + 1, name: req.body.name };
  users.push(user);
  res.status(201).json(user);            // status + json chained
});

app.listen(3000, () => console.log('Server on 3000')); // internally uses http.createServer

Project 1: CRUD REST API

What we're building: A full CRUD REST API for a /todos resource with Express — list all, get one, create, update, delete. Data lives in a plain in-memory array (a real database is the natural next step, noted at the end). This is THE resume project for a backend fresher: it wires together express.json() body parsing + the 5 CRUD routes + req.params/req.body + correct status codes + a 404 handler — the exact skeleton every backend job expects you to build in an interview.

By the end you'll trace a request straight through: request comes in → Express matches the route → the handler reads req.params/req.body → we mutate the array → res.status(code).json(...) replies. If you did the Node project-file-todo-api (the raw http version), watch how Express deletes almost all the plumbing you wrote by hand.

🌍 Real-world example: This is exactly what a to-do app's backend does. The frontend sends POST /todos with {"title":"buy milk"}, the server creates it and replies 201 Created with the new todo (including its id). Later it sends GET /todos to show the list. Swap the array for a Mongo/Postgres call and this is production.

💡 CRUD = Create, Read, Update, Delete — the four things you do with any resource. In REST they map to POST, GET, PUT/PATCH, DELETE on a noun URL like /todos.

💡 REST API = a set of URL + HTTP-method rules for working with data. The method is the verb (GET read, POST create…), the URL is the noun (/todos, /todos/3).


Step 0 — The mental model (read this first)

In raw Node you wrote one giant handler and matched req.method + req.url with a wall of ifs, split the URL by hand for :id, and collected the body in stream chunks. Express replaces all three:

  • Routingapp.get('/todos', handler), app.post('/todos', handler). No method/url ifs. Express matches the verb + path and calls the right function.
  • Route params — a :id in the path (/todos/:id) shows up parsed on req.params.id. No url.split('/').
  • Body parsingapp.use(express.json()) reads the JSON body for you and hands it over as req.body. No req.on('data') chunk-collecting.

And replies get shorter: res.status(201).json(obj) sets the status and sends JSON in one chainable line. Same REST concepts, a fraction of the code.


Step 1 — The app + express.json()

Every Express app starts the same three lines:

const express = require('express');
const app = express();

app.use(express.json()); // parse JSON bodies into req.body

What's happening:

  1. express() creates the app — an object that is also the request handler Node's http server will call. (Under the hood app.listen still uses http.createServer — Express sits on top of the same http module you learned.)
  2. app.use(express.json()) registers a middleware that runs on every request before your routes. It looks at requests with Content-Type: application/json, reads the body, JSON.parses it, and sets req.body. Without this line, req.body is undefined — the single most common Express gotcha.
  3. Order matters: express.json() must be registered above the routes that read req.body, because middleware runs top-to-bottom.

💡 Middleware = a function that runs during the request→response cycle, before (or instead of) your route handler. express.json() is a built-in one; you can write your own (req, res, next) => {}.


Step 2 — Our data + the 5 CRUD routes

We keep todos in a normal array and an id counter — no file, no DB (yet):

let todos = [];
let nextId = 1;

Now the five routes. Each maps one HTTP verb to one CRUD action:

// READ all
app.get('/todos', (req, res) => {
  res.status(200).json(todos);
});

// READ one
app.get('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Todo not found' });
  res.status(200).json(todo);
});

// CREATE
app.post('/todos', (req, res) => {
  const { title } = req.body;
  if (!title) return res.status(400).json({ error: 'title is required' });
  const todo = { id: nextId++, title, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

// UPDATE
app.put('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Todo not found' });
  if (req.body.title !== undefined) todo.title = req.body.title;
  if (req.body.done !== undefined) todo.done = req.body.done;
  res.status(200).json(todo);
});

// DELETE
app.delete('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const before = todos.length;
  todos = todos.filter(t => t.id !== id);
  if (todos.length === before) return res.status(404).json({ error: 'Todo not found' });
  res.status(204).end();
});

What's happening — the whole CRUD table at a glance:

  • GET /todos → read the list → 200 OK with the array.
  • GET /todos/:idreq.params.id gives the id → find it → 200 with the todo, or 404 if it's not there.
  • POST /todos → read req.body.title → validate → create → 201 Created (the "I made a new resource" code).
  • PUT /todos/:id → find + update the given fields → 200, or 404.
  • DELETE /todos/:id → remove it → 204 No Content (success, nothing to send back), or 404.

Notice: no url.split, no req.on('data'), no manual Content-Type header. Express gave us req.params.id, req.body, and res.json().

💡 res.status(code).json(obj) is chainablestatus() returns res, so you set the code and send the body in one line. res.json() also sets Content-Type: application/json for you.

💡 204 No Content for DELETE: the operation succeeded but there's nothing meaningful to return, so we send an empty body with .end(), not .json().


Step 3 — TRACE a POST end to end (body → validate → create → 201)

This is the interview money-shot. Follow every beat — client sends POST /todos with {"title":"buy milk"}:

  1. Middleware runs firstexpress.json() sees Content-Type: application/json, reads the body stream, JSON.parses it, and sets req.body = { title: 'buy milk' }. Then it calls next(), handing control to the router.
  2. Route match → Express sees POST + /todos and calls the app.post('/todos', ...) handler. (No if (method === 'POST' && url === '/todos') — the framework matched it.)
  3. Read the bodyconst { title } = req.body pulls out 'buy milk'. This only works because express.json() ran in step 1 — remove that middleware and req.body is undefined and this throws.
  4. Validate → no title? return res.status(400).json({ error: 'title is required' }) and stop. (Client sent bad data → 400.) The return is important — it stops the handler so we don't also send a 201.
  5. Create → build { id: 1, title: 'buy milk', done: false } (id from nextId++, which is 1 then becomes 2) and push it into todos.
  6. Replyres.status(201).json(todo) — status 201 Created, body is the new todo with its id. The client now knows the id it can use for future GET/PUT/DELETE.

Prove it with a read-back: the client sends GET /todos. That handler runs res.status(200).json(todos) → the array now holds [{ id: 1, title: 'buy milk', done: false }] → returned. (In-memory means it's gone on restart — that's the one thing a real DB fixes, and the reason Step 5 points there.)

💡 Contrast with raw Node: in the http version this same POST needed a hand-written parseBody(req) that collected req.on('data') chunks and JSON.parsed on 'end', plus a manual if (method === 'POST' && url === '/todos'). Express did both for free — that's the whole point of the framework.


Step 4 — TRACE a GET /:id end to end (param → find → 200 or 404)

Client sends GET /todos/1:

  1. Route match → Express matches GET against the pattern /todos/:id. The :id segment is a route param; Express parses 1 out of the path and sets req.params = { id: '1' }.
  2. Read the paramconst id = Number(req.params.id). Route params are always strings ('1'), so we Number() it to 1 to compare against the numeric todo.id. (Same reason query values are strings.)
  3. Findtodos.find(t => t.id === id) looks for a todo whose id matches.
  4. Branch on the result:
    • Foundres.status(200).json(todo)200 OK with that single todo.
    • Not foundreturn res.status(404).json({ error: 'Todo not found' })404, because the id doesn't exist. The return stops us from falling through and sending a second response (calling res twice throws "Cannot set headers after they are sent" — another classic gotcha).

Compare to raw Node: there you did Number(url.split('/')[2]) to dig the id out of /todos/1. Express handed it to you as req.params.id from the :id in the route path. The router does the URL parsing.


Step 5 — The 404 handler + a note on DB

What if the client hits a route we never defined, like GET /users? Add a catch-all at the very bottom, after all real routes:

// 404 — no route matched
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.listen(3000, () => console.log('API on http://localhost:3000'));

What's happening: middleware runs top-to-bottom. If a request doesn't match any of the five routes above, it falls through to this final app.use(...), which replies 404. Because it has no path, it matches everything that got this far — so it must be registered last, or it would swallow requests meant for real routes (middleware-order gotcha).

The DB next step: right now let todos = [] lives in memory, so every restart wipes the data (and it doesn't scale past one server process). The natural upgrade is to replace the array with a database: todos.find(...) becomes Todo.findById(id) (Mongoose/MongoDB) or a SELECT ... WHERE id = $1 (Postgres). Every route's shape stays identical — the handler, status codes, and req.params/req.body don't change; only the data-access line does. That's why this array version is a perfectly honest resume project: it is the real structure, minus one swap.


🔎 The full flow (recap the wiring)

Every route is the same three-beat song, now Express-short:

  1. Match — Express routes the verb + path to your handler (app.get/post/put/delete). No manual ifs.
  2. Read + changereq.params.id (which resource) and req.body (the new data, thanks to express.json()); mutate the array (find/push/filter).
  3. Replyres.status(code).json(...) with the right code: 200 read/update, 201 create, 204 delete, 400 bad input, 404 not found.

🚀 From raw Node to Express — what disappeared

Raw http version Express version
if (method === 'GET' && url === '/todos') app.get('/todos', ...)
Number(url.split('/')[2]) req.params.id
hand-written parseBody(req) (stream chunks) app.use(express.json())req.body
res.writeHead(201); res.end(JSON.stringify(x)) res.status(201).json(x)
res.setHeader('Content-Type', ...) on every reply res.json() sets it for you

Same REST verbs, same status codes, same data logic — Express just deleted the boilerplate. That's why we built it the hard way first: now you know exactly what Express is doing under the hood.


✅ What you just learned

  • A full CRUD REST API in Expressapp.get/post/put/delete mapping the five verbs to Create/Read/Update/Delete on /todos.
  • express.json() — the built-in middleware that populates req.body; without it req.body is undefined.
  • req.params vs req.body — the id from the URL path (:id) vs the JSON payload; params are strings, so Number() them.
  • Correct status codes200 read/update, 201 create, 204 delete, 400 bad input, 404 not found.
  • A 404 catch-all registered last so it only catches truly unmatched routes.
  • The return res... habit so you never send two responses ("headers already sent").
  • The bridge from raw Node — Express removes routing + body-parsing boilerplate; the DB swap is the next step.
const express = require('express');
const app = express();

app.use(express.json()); // parse JSON bodies into req.body

let todos = [];
let nextId = 1;

// READ all
app.get('/todos', (req, res) => {
  res.status(200).json(todos);
});

// READ one
app.get('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Todo not found' });
  res.status(200).json(todo);
});

// CREATE
app.post('/todos', (req, res) => {
  const { title } = req.body;
  if (!title) return res.status(400).json({ error: 'title is required' });
  const todo = { id: nextId++, title, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

// UPDATE
app.put('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const todo = todos.find(t => t.id === id);
  if (!todo) return res.status(404).json({ error: 'Todo not found' });
  if (req.body.title !== undefined) todo.title = req.body.title;
  if (req.body.done !== undefined) todo.done = req.body.done;
  res.status(200).json(todo);
});

// DELETE
app.delete('/todos/:id', (req, res) => {
  const id = Number(req.params.id);
  const before = todos.length;
  todos = todos.filter(t => t.id !== id);
  if (todos.length === before) return res.status(404).json({ error: 'Todo not found' });
  res.status(204).end();
});

// 404 — no route matched (register LAST)
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.listen(3000, () => console.log('API on http://localhost:3000'));

Express.jsinterview questions & answers

10 sample questions below — 163+ in the full bank inside.

What is error-handling middleware in Express?

It is a special middleware with the four-argument signature (err, req, res, next). Express routes any error passed via next(err) to it, so instead of every route repeating try/catch logic, you handle all errors in one central place and send a clean response.

In simple terms: Think of a factory conveyor belt: normal middleware are the workers passing the item along, and the error handler is the reject bin at the very end where any faulty item is dropped. Example: app.use((err, req, res, next) => res.status(500).json({ error: err.message })); every next(err) in your app lands here.

How do you enable CORS in an Express app?

Install the `cors` package and register it as middleware with `app.use(cors())` before your routes. It automatically adds the Access-Control-Allow-Origin (and related) headers to responses. You can pass options like `cors({ origin: 'http://localhost:3000' })` to whitelist a specific origin instead of allowing everyone.

In simple terms: The cors middleware is like hiring a doorman who stamps the right 'allowed' sticker on every response so the browser lets it through. Example: ```js const cors = require('cors'); app.use(cors({ origin: 'http://localhost:3000' })); ``` Now requests from the React app on :3000 are allowed.

What does app.use() do?

app.use() registers application-level middleware that runs for every incoming request (for all HTTP methods). If you pass a path, like app.use('/api', fn), it only runs for requests whose URL starts with that path. It's the main way you plug middleware into the app.

In simple terms: app.use() is like adding a rule to the office front-desk that applies to everyone walking in — 'everyone must sign the register'. Example: app.use(express.json()) makes JSON body-parsing run for every request, so req.body is filled before your route handler sees it.

What is the difference between express.json() and express.urlencoded()?

Both are built-in body parsers, but they handle different Content-Types. express.json() parses application/json bodies; express.urlencoded() parses application/x-www-form-urlencoded (HTML form) bodies. Apps often register both so they accept either format.

In simple terms: Same job (fill req.body), different input formats — like having two chargers, one USB-C and one micro-USB, for two kinds of phones. Example: app.use(express.json()); app.use(express.urlencoded({ extended: true })); now the server understands both JSON API calls and plain form submits.

What does express.json() do?

express.json() is a built-in middleware that reads the incoming request body, and if its Content-Type is application/json, parses the JSON text into a JavaScript object and puts it on req.body. You register it once with app.use(express.json()).

In simple terms: It's a translator that turns JSON text coming over the wire into a real JS object you can use. Text comes in, object goes onto req.body. Example: app.use(express.json()); then in a handler req.body is the parsed object like { email: 'a@b.com' } instead of undefined.

What is the same-origin policy?

The same-origin policy is a browser security rule that stops a web page from reading responses of requests made to a different origin than the page itself. An origin is the combination of protocol + domain + port; if any of those three differ, it is a different origin. It exists to protect users so a malicious site can't silently read data from another site you're logged into.

In simple terms: Think of origins like separate apartments in a building — you can knock on any door, but you can't just walk in and read someone else's mail. The browser enforces the walls. Example: `http://site.com` and `https://site.com` are different origins (protocol differs), and so are `site.com:3000` and `site.com:5000` (port differs).

When would you use express.urlencoded() instead of express.json()?

express.urlencoded() parses bodies sent as application/x-www-form-urlencoded — the format a classic HTML <form> submits. Use it for traditional form posts; use express.json() when the client sends JSON (like a fetch/axios call with a JSON body).

In simple terms: A plain HTML form doesn't send JSON — it sends data like name=Ali&age=22 (key=value pairs joined by &). express.urlencoded() decodes that string back into req.body. Example: app.use(express.urlencoded({ extended: true })) turns name=Ali&age=22 into req.body = { name: 'Ali', age: '22' }.

You send a POST request with a JSON body but req.body is undefined. Why?

Express does not parse the request body automatically. Without a body-parsing middleware, req.body stays undefined. Adding app.use(express.json()) before your routes parses the incoming JSON into req.body.

In simple terms: The request body arrives as a raw stream of bytes; Express won't turn it into a usable object unless you tell it to. Think of a sealed courier parcel — the postman drops it at your door but won't unwrap it; express.json() is the person who opens it and hands you the contents. Example: after app.use(express.json()), a POST with {"name":"Ali"} gives you req.body.name === 'Ali'.

What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser mechanism that lets a server explicitly allow requests coming from a different origin (different protocol, domain, or port). By default browsers block cross-origin requests due to the same-origin policy; CORS is the controlled way to relax that by having the server send Access-Control-Allow-Origin headers.

In simple terms: Think of it like a guest list at a party: your API is the party, and by default only guests from the same house (same origin) get in. CORS is the host adding other addresses to the guest list. Example: an API at http://localhost:5000 can send `Access-Control-Allow-Origin: http://localhost:3000` to let the React app on :3000 in.

How does Express know a middleware is an error handler?

Purely by the number of parameters — if the function declares exactly four (err, req, res, next), Express treats it as an error-handling middleware. Drop the err and it becomes a normal three-arg middleware, so the count is not optional.

In simple terms: It is like a form with a mandatory 'complaint' box: a form with that extra box goes to the complaints desk, one without it goes to the normal counter. Example: (err, req, res, next) => {} is an error handler; (req, res, next) => {} is a regular one — same body, different desk.

153+ more Express.js questions inside

Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.

Unlock all questions — free

Ready to practise Express.js?

Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.