AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Express.js: Building Web Applications

📚 Backend Development⏱️ 24 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 24 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Why One Web Page Cannot Answer Every Student Differently

Suppose your school wants a results page where any one of 1,200 students can type in their roll number and instantly see their own marks — and nobody else's. Every static page you have built so far works the opposite way: you write one HTML file, save it on a server, and every single visitor who opens it gets back the exact same bytes, word for word. That is fine for an "About Us" page or a fee-structure notice. It completely breaks down for a marks-lookup page, because the correct answer is different for every one of those 1,200 roll numbers, and it changes every exam cycle. You cannot pre-write 1,200 separate HTML files, and even if you somehow did, admitting one new student next year would mean editing your entire website by hand.

What you actually need is a program that keeps running on the server, waits for a browser to ask a question ("what are the marks for roll number 107?"), looks up the answer, builds a response on the spot, and sends it back. Code that does this job — running on the server, computing a fresh answer per request — is called backend code. Express.js is the most widely used toolkit in the JavaScript world for writing it, and this chapter builds it up from first principles: what a server actually is, why raw server code gets messy fast, and how Express organizes that mess into something readable.

The Machine Behind the Page: Node.js and the http Module

Until now, every line of JavaScript you have written has run inside a browser, reacting to clicks and typing. Node.js is a program that runs that same JavaScript language outside the browser — directly on a computer, as a standalone process, the way Python or Java programs run. Node ships with a built-in module called http that lets a program open a "port" (think of it as a numbered mailbox on the computer) and listen for incoming requests from browsers anywhere on the network.

Here is a complete, working web server using nothing but that raw http module — no Express yet:

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Welcome to AI Computer Institute');
  } else if (req.url.startsWith('/marks/') && req.method === 'GET') {
    const roll = req.url.split('/')[2];
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end(`Looking up marks for roll number ${roll}`);
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Page not found');
  }
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

Trace what happens when a browser requests GET /marks/107: Node calls the function passed to createServer with two objects — req (the incoming request, holding req.url = '/marks/107' and req.method = 'GET') and res (a handle you use to write the reply). The code checks each if condition top to bottom. The first fails (url isn't exactly '/'). The second succeeds, so it manually chops the URL apart with .split('/') to dig out "107", then writes back a 200 response.

This works, but notice the two problems that get worse as the site grows. First, every route has to hand-parse the URL itself — extracting "107" from /marks/107 used .split('/')[2], a fragile trick that would silently break if someone requested /marks/107/ with a trailing slash. Second, the routing logic is one long, growing if / else if chain. A real school site needs a home page, a marks-lookup page, a toppers list, a teacher login, an attendance page — each new route means another else if squeezed into an already-long chain, and a single typo in a URL string fails silently at runtime with no warning from the editor.

What Express.js Actually Is

Express is not a separate program you install and run the way you'd install MySQL Workbench or a database server, and it is not a new programming language. It is an ordinary npm package — literally JavaScript source code — that you pull into your own file with require('express'), exactly like any other library. It runs inside the very same Node.js process as the rest of your code; it is a thin, well-designed layer built on top of Node's own http module that replaces hand-written if/else chains with a clean, declarative way of saying: "when a GET request arrives at exactly this path, run this function."

Here is the same welcome route, rewritten with Express:

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

app.get('/', (req, res) => {
  res.send('Welcome to AI Computer Institute');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

express() creates an "app" object — think of it as a routing table that Express manages for you. app.get('/', ...) registers a rule: "if a GET request's path is exactly /, run this function." app.listen(3000, ...) starts the server on port 3000, the same job server.listen did in the raw version. Notice there is no manual URL parsing and no if/else chain — Express does the matching internally, and you simply declare what each route should do.

req and res: The Two Objects Every Handler Receives

Every route function you write in Express — no exceptions — receives exactly two objects, always in this order: req and res. req (the request) describes what the browser asked for: which URL, which HTTP method, what data it sent along. res (the response) is your toolkit for answering: res.send(text) sends plain text or HTML, res.json(object) converts a JavaScript object to JSON text and sends it with the correct headers, and res.status(code) sets the HTTP status number before sending. These three methods — send, json, and status — are the ones you will reach for constantly, so it is worth knowing them by name before moving on.

Route Parameters: One Route That Answers for Every Roll Number

The raw-Node version above hand-chopped the roll number out of the URL string. Express has a built-in feature for exactly this situation, called a route parameter: you write a colon followed by a name inside the path, and Express automatically captures whatever text appears at that position in the real URL.

const marksDatabase = {
  101: { name: 'Aditi Sharma', marks: 88 },
  102: { name: 'Rohan Verma', marks: 76 },
  107: { name: 'Kavya Iyer', marks: 92 }
};

app.get('/marks/:roll', (req, res) => {
  const roll = req.params.roll;                 // "107" — always a string
  const student = marksDatabase[roll];

  if (student) {
    res.status(200).json({
      rollNumber: Number(roll),
      name: student.name,
      marks: student.marks
    });
  } else {
    res.status(404).json({ error: `No student found with roll number ${roll}` });
  }
});

Trace this for a request to GET /marks/107. Express compares the incoming path /marks/107 against the registered pattern /marks/:roll. The literal part, /marks/, matches character for character; the :roll segment is a wildcard that matches whatever remains, "107", and Express stores that captured text as req.params.roll. Inside the handler, roll is the string "107" — not the number 107 — because everything that ever appears inside a URL is text; there is no way for the browser to send an actual number over HTTP, only characters.

The next line, marksDatabase[roll], looks up the property named "107" on the marksDatabase object and finds it, because JavaScript object keys are always strings (or Symbols) internally. Even though the object literal was written with what look like number keys — 101, 102, 107 — JavaScript automatically converts each of them to the string "101", "102", "107" the moment the object is created. So marksDatabase[107] and marksDatabase["107"] are exactly the same lookup, and the string that arrived from req.params.roll matches directly — no conversion is needed for the lookup to succeed.

Common Misconception: "Number() Is Needed to Find the Record"

It is tempting to look at the line rollNumber: Number(roll) and conclude that the conversion is what makes marksDatabase[roll] work. That is incorrect, and it teaches the wrong mental model of how JavaScript object keys behave. The lookup marksDatabase[roll] already succeeds perfectly well with roll as the plain string "107", precisely because of the key-coercion rule explained above. Delete the Number(...) call from the lookup entirely and the record is still found.

So what is Number(roll) actually for? It changes how the value is typed in the JSON response, not whether the record is found. Compare:

JSON.stringify({ rollNumber: "107" })   // {"rollNumber":"107"}  — quoted string
JSON.stringify({ rollNumber: 107   })   // {"rollNumber":107}    — bare number

Without the conversion, res.json({ rollNumber: roll, ... }) would send "rollNumber":"107" — a quoted string — to the browser, even though a roll number is conceptually a number. Any code on the receiving end that expects to do arithmetic on it (sort students by roll number, compare it numerically) would get subtly wrong results, because "107" < "20" is true under string comparison while 107 < 20 is false under numeric comparison. Number(roll) exists to produce correctly typed output data, not to make an object lookup succeed.

The Request's Journey Through an Express App

The diagram below traces one concrete request, GET /marks/107, all the way from the browser through Express's pipeline to one of two possible outcomes.

Express request lifecycle: GET /marks/:roll Browser (client) GET /marks/107 Express app — app.listen(3000) app.use(express.json()) middleware: parses JSON body into req.body app.get('/marks/:roll', handler) req.params.roll = "107" (always a string) Does marksDatabase["107"] exist? Yes No 200 OK - match found res.status(200).json({ rollNumber: 107, ... }) 404 Not Found - no match res.status(404).json({ error: ... }) Browser receives the JSON response and renders the result on screen

Every box in this diagram is a step the request must pass through, in order, from top to bottom, before either outcome box is reached. That top-to-bottom order is not incidental — it is the exact mechanism the next section explains.

Query Strings vs. Route Parameters

Express has a second way to pass data through a URL, and it is easy to confuse with route parameters. A query string is the part after a ?, such as /marks?roll=107. Express makes this available as req.query.roll, also always a string. The practical difference is what each is for: a route parameter like /marks/:roll identifies a specific resource — this exact student's record — and is treated as a required part of the path. A query string is used for optional extras layered on top of a request, such as /marks?subject=maths&sort=desc to filter or sort a list. If leaving the value out should make the URL invalid, use a route parameter; if leaving it out should just mean "use the default," use a query string.

Middleware: The Pipeline Every Request Walks Through

Look again at the diagram: before the request ever reaches the route matcher, it passes through a box labelled app.use(express.json()). This is Express's second core idea, called middleware. A middleware function has almost the same shape as a route handler, but with one extra parameter: (req, res, next) => { ... }. Registering one with app.use(...) means "every request, regardless of its path, passes through this function first." Inside, the function can inspect or modify req, and it must call next() to hand control forward to whatever comes after it. Forget to call next(), and the request simply stops there forever — the browser sits with a spinning loading icon and no response ever arrives, because nothing told Express to continue.

express.json() is a middleware function built into Express itself. Its one job is: if the incoming request has a JSON body attached (common for POST requests), read and parse it, then attach the result as req.body, ready for any later route handler to use directly as a JavaScript object.

Accepting Data With POST: Adding a New Student Record

Everything so far has only read existing data. Adding a new record needs the browser to send data to the server, which is what the POST method and req.body are for:

app.use(express.json());   // must be registered before routes that read req.body

app.post('/marks', (req, res) => {
  const { roll, name, marks } = req.body;

  if (!roll || !name || marks === undefined) {
    return res.status(400).json({ error: 'roll, name and marks are all required' });
  }

  marksDatabase[roll] = { name, marks };
  res.status(201).json({ message: `Record created for roll number ${roll}` });
});

Trace a request: a client sends POST /marks with a JSON body {"roll": 108, "name": "Zara Khan", "marks": 81}. Because app.use(express.json()) was registered earlier in the file, it runs first for every request, parses that JSON text, and sets req.body to the matching JavaScript object. The app.post('/marks', ...) handler then runs, destructures roll, name, and marks out of req.body, checks that none of the required fields are missing, and — if all three are present — writes a new property onto marksDatabase keyed by roll (again auto-converted to the string key "108"), then replies with status 201 and a confirmation message.

Common Misconception: Middleware and Route Order Don't Matter

Some beginners assume Express somehow figures out on its own which middleware applies to which route, in any order you write them. It does not. Express runs registered middleware and route handlers in exactly the order they appear in your file, from top to bottom, for every incoming request — this is why the code comment above insists express.json() be registered before app.post('/marks', ...). If you swapped the two lines, a POST request would reach the route handler before the JSON-parsing middleware ever ran, req.body would still be undefined, and destructuring it would throw a runtime error. Order in the file is not cosmetic — it is the sequence of steps the request physically walks through.

HTTP Status Codes: Telling the Browser What Happened

Every response in the examples above sets a numeric status code before sending data. These numbers are not arbitrary — they follow a standard that every browser, and every piece of code that talks to a server, understands:

  • 200 OK — the request succeeded and here is the data.
  • 201 Created — a new resource was successfully created (used after a POST that adds a record).
  • 400 Bad Request — the client sent data that is missing or malformed; the server refuses to process it.
  • 404 Not Found — the requested path, or the specific record within it, does not exist.
  • 500 Internal Server Error — something broke inside the server's own code while handling an otherwise valid request.

Choosing the right status code is not decoration — code on the receiving end (a mobile app, a frontend script, another server) branches its behaviour based on this number, often without even reading the response body.

Common Misconception: "Express" Is a Separate Server Program

Because the word "Express" sounds like the name of standalone software — the way MySQL or Apache are standalone programs you install and start separately — many beginners picture it that way. It is not. There is no "Express server" running anywhere on your computer as its own process. Express is plain JavaScript source code, pulled into your file with a single require('express'), and it executes inside the identical Node.js process that runs the rest of your program. When your program exits, Express exits with it — there is nothing left running independently in the background.

Where This Fits in Your CBSE Syllabus

The client–server model this chapter builds on — a browser sending a request, a server computing and returning a response, identified by a method and a status code — is the same model tested in the CBSE Class 11–12 Computer Science networking unit, which covers client-server architecture and protocols like HTTP. Express does not change that underlying model; it simply gives you a fast, organized way to write the server side of it in JavaScript, which is why the request/response vocabulary you have just learned — GET, POST, status codes, request and response objects — transfers directly to board-exam questions about web architecture, even those that never mention Express by name.

Putting It All Together

Here is the complete file, combining every piece covered above into one working server:

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

app.use(express.json());

const marksDatabase = {
  101: { name: 'Aditi Sharma', marks: 88 },
  102: { name: 'Rohan Verma', marks: 76 },
  107: { name: 'Kavya Iyer', marks: 92 }
};

app.get('/', (req, res) => {
  res.send('Welcome to AI Computer Institute');
});

app.get('/marks/:roll', (req, res) => {
  const roll = req.params.roll;
  const student = marksDatabase[roll];

  if (student) {
    res.status(200).json({ rollNumber: Number(roll), name: student.name, marks: student.marks });
  } else {
    res.status(404).json({ error: `No student found with roll number ${roll}` });
  }
});

app.post('/marks', (req, res) => {
  const { roll, name, marks } = req.body;

  if (!roll || !name || marks === undefined) {
    return res.status(400).json({ error: 'roll, name and marks are all required' });
  }

  marksDatabase[roll] = { name, marks };
  res.status(201).json({ message: `Record created for roll number ${roll}` });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Check Your Understanding

  1. Using the marksDatabase above, trace what the server sends back — both the status code and the JSON body — for GET /marks/103.
  2. A classmate writes app.post('/marks', ...) on line 3 of their file and app.use(express.json()) on line 10. When they test it with Postman, req.body is always undefined. Explain exactly why, using what you now know about middleware order.
  3. Rewrite the line rollNumber: Number(roll) as rollNumber: roll (removing the conversion) and predict the exact JSON text res.json would send for roll number 107. Would the record still be found? Would the JSON output be identical to the original? Justify both answers separately.
  4. A route is written as app.get('/students/:id', handler). What is the data type of req.params.id when a browser requests /students/45 — number or string? What is the data type of req.query.id for a request to /students?id=45?
  5. Add a fourth entry with roll number 110 to marksDatabase, then trace, box by box using the diagram's structure, what happens for GET /marks/110 from the moment the request leaves the browser to the moment a response is displayed.

Summary

  • Static HTML files cannot answer differently per visitor; backend code running on a server is needed whenever the response depends on input, like a roll number.
  • Node.js runs JavaScript outside the browser; its built-in http module can build a server, but routing by hand with if/else chains and manual URL parsing becomes fragile and unwieldy as routes multiply.
  • Express.js is an npm package — plain JavaScript running inside the same Node process — that replaces hand-written routing with declarative calls like app.get(path, handler) and app.post(path, handler).
  • Every route handler receives req (the incoming request) and res (your tool for responding), in that fixed order.
  • Route parameters (:roll) capture dynamic parts of a URL as strings in req.params; query strings (?roll=107) pass optional data via req.query, also as strings.
  • Object keys in JavaScript are always strings, so a numeric-looking key like marksDatabase[107] and its string form marksDatabase["107"] refer to the identical property — no conversion is needed for a lookup to succeed. Number(...) is used to control the type of a value in JSON output, not to make a lookup work.
  • Middleware functions, registered with app.use, run in the exact order they are written, for every request, before matching routes; forgetting to call next() stalls the request permanently, and registering middleware after the route that needs it means that route never benefits from it.
  • HTTP status codes (200, 201, 400, 404, 500) communicate outcome to whatever is consuming the response, and choosing the correct one is part of writing correct backend code, not a stylistic afterthought.

Think About It

Think about this: How would you explain express.js: building web applications to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where express.js: building web applications is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting express.js: building web applications to at least 3 other topics you have studied.
← Node.js: Running JavaScript on the ServerExpress Middleware: Processing Requests →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn