Article

Backend Engineering from Zero to Production

Learn what a backend really is, how the web talks (DNS, HTTP, cookies, CORS), then build your first Node.js and Express server with routes, controllers and middleware. One easy episode a day.

Devdeep15 chapters 91 min readUpdated Nov 4, 2026

Chapter 1 ยท Oct 21, 2026

Day 1: What Is Backend, and Why Do We Need It?

backend-seriesbasics

Key takeaway

Frontend shows, backend decides, database remembers; never put database credentials or business rules in client code.

1. The Restaurant Analogy ๐Ÿฝ๏ธ

You sit in the dining area, look at the menu, and tell the waiter your order. You never walk into the kitchen. The kitchen cooks, follows the rules, and fetches ingredients from the storeroom.

A web application works the same way:

RestaurantWeb applicationWhat it does
Dining area, menu, waiterFrontendWhat you see and tap
KitchenBackendThe logic and the rules
StoreroomDatabaseWhere data is kept

You never need to know how the kitchen works to enjoy your meal, and the kitchen never lets customers walk in and cook for themselves. That separation is the whole idea of a backend.

In short: Frontend is what users see. Backend is the hidden part that does the real work.

2. Client and Server ๐Ÿ–ฅ๏ธ

A client is anything that asks for something, like your browser or a phone app.

A server is a computer that stays on and connected, waiting for requests and answering them.

The client sends a request, the server processes it, and the response goes back to be shown to the user.

A server isn't magic. It's just a computer running a program that listens for requests. Here is a complete one in a few lines of Node.js (we will build this properly on Day 11 and Day 12):

js
const http = require("http"); http.createServer((req, res) => { res.end("Hello from the backend!"); }).listen(3000);

Run it and open http://localhost:3000. Your browser is the client, this little program is the server.

Key insight: "Server" can mean the program that answers requests or the machine it runs on. Both meanings are common.

In short: Client asks, server answers. That's the request-response model, and nearly everything online uses it.

3. What Lives Where? ๐Ÿ—บ๏ธ

WhereWhat lives thereExamples
Your device (frontend)Layout and interactionButtons, forms, animations
The server (backend)Decisions and rulesLogin checks, business rules, payments, talking to the database
The databaseData that must be rememberedUsers, posts, orders

Here's a twist that surprises most beginners: the backend is also a client. When it needs data, it sends its own request to the database server. So one server can act as both client and server at the same time.

In short: Frontend shows, backend decides, database remembers.

4. Static vs Dynamic ๐Ÿ“„

A static server sends stored files exactly as they are. Everyone who asks for about.html gets the same file.

A dynamic server has extra software, usually an application server and a database, that builds the page before sending it.

StaticDynamic
What is sentA stored file, as-isA response built for this request
Same for everyone?YesNo, it can depend on who you are
Needs a backend?NoYes
ExampleA portfolio pageYour Instagram feed

Instagram can show your feed because a backend builds it for you each time. Sites like Wikipedia are mostly a few templates plus a huge database, rather than millions of separate pages written by hand.

In short: Static means the same file for everyone. Dynamic means built fresh per request, and that needs a backend.

5. Why Not Let the Frontend Talk to the Database Directly? ๐Ÿ”

It sounds simpler to skip the backend. Here's why we don't:

ProblemWhat goes wrong without a backend
SecurityConnection details stored in client code can be stolen. Anyone can open your app's code and read it.
ScalingIf every user connects straight to one database, it gets overwhelmed once enough people are using the app.
ChangeWith a backend in between, you can change the database structure, or even switch databases, without breaking the app.
Rules in one placeOtherwise business logic gets duplicated in every client, which makes updates hard. One backend can serve web, Android and iOS.

One honest nuance: services like Firebase or Supabase do let apps talk to a database-like service. But that works because a secured gateway enforces rules on the server side, not because the browser reaches a raw database. A backend is still doing the guarding.

Key insight: Anything shipped to the user's device can be read and changed by the user. Keys and rules belong on the server.

In short: The backend is the gatekeeper. It protects data, enforces rules, and lets many apps share one system.

6. What Breaks Without This? ๐Ÿ’ฅ

Imagine putting this in your frontend JavaScript:

js
// โŒ Never do this const db = connect("mongodb://admin:SuperSecret123@db.myshop.com:27017/shop");

Now open the site, press Ctrl + U (View Source) or check the Sources tab in DevTools. The password is right there for everyone to read. Anyone can now connect to your database and read, change or delete everything.

The fix is the backend: the frontend calls GET /api/orders, and only the server, which nobody else can see, holds the database password.

Common mistakes

  • Thinking "nobody will look at my JavaScript". Anyone can, in one click.
  • Hiding a secret in an environment variable that the build tool still ships to the browser.
  • Believing a request is safe because it came from your frontend. Requests can be sent by anyone.

๐Ÿงช Try it yourself

  1. Open any website, press F12, and go to the Network tab. Reload the page. Every line you see is the client sending a request and the server answering.
  2. Click one request and look at Response. That is exactly what the backend sent back.
  3. Open the Sources tab and browse the JavaScript files. Everything there is public, which is why secrets never go in the frontend.

๐ŸŽฏ Day 1 in one line

Frontend shows things, backend makes decisions, and the database stores data. You never trust the client with the keys.

Frontend vs BackendClient-Server ModelRequest-Response CycleStatic vs DynamicDatabase
Next chapterDay 2: How Your Request Finds the Server (IP, DNS and Ports)

Chapter 2 ยท Oct 22, 2026

Day 2: How Your Request Finds the Server (IP, DNS and Ports)

backend-seriesbasicsnetworking

Key takeaway

DNS turns a domain name into an IP address (where the server is), and the port tells the server which service gets the traffic: 443 for HTTPS, 80 for HTTP. localhost always means your own machine.

You type youtube.com into your browser. How does your request find the right server, out of millions of machines on the internet?

Let's break it down.

1. IP Address: Where Is the Server? ๐Ÿ“

An IP address identifies a network interface and helps route data to its destination.

Think of it as a building's address. Without it, the internet would not know where to deliver your request.

text
142.250.190.14

Your browser uses the server's IP address to communicate with it.

The example above is an IPv4 address (four numbers separated by dots). You will also see longer IPv6 addresses, such as 2001:db8::1. They do the same job, there are just far more of them available.

Key insight: Computers do not find each other by name. They find each other by IP address.

In short: An IP address tells the network where the destination is.

2. DNS: Finding the IP Address ๐Ÿ”Ž

Humans remember domain names. Networks use IP addresses to route traffic.

DNS (Domain Name System) translates a domain name into its associated IP address.

text
youtube.com โ†’ 142.250.190.14

Think of DNS as your phone's contact list. You search for "Mom" instead of remembering her number. DNS lets you type youtube.com instead of remembering a string of digits.

Phone contactsDNS
Contact name ("Mom")Domain name (youtube.com)
Phone numberIP address
Looking up the contactDNS lookup

DNS answers are usually cached for a while, by your browser, your operating system and the DNS resolver. That is why the lookup is often instant the second time you visit a site.

In short: DNS answers the question "Which IP belongs to this domain?"

3. Ports: Which Application Gets the Traffic? ๐Ÿšช

A single server can run multiple applications at the same time: a web server, a Node.js API, a database, and more. Ports help the operating system direct incoming traffic to the right service.

PortTypically used for
:80Default HTTP port
:443Default HTTPS port
:3000Commonly used by Node.js development servers
:5432PostgreSQL's default port
:27017MongoDB's default port

An IP address and a port are often written together:

text
192.168.1.10:3000
  • IP = Building address
  • Port = Specific room inside the building

When you leave the port out of a URL, the browser uses the default. So https://example.com really means https://example.com:443.

In your own backend, you choose the port your app listens on:

js
import express from "express"; const app = express(); app.get("/", (req, res) => { res.send("Hello from port 3000!"); }); app.listen(3000, () => { console.log("Listening on http://localhost:3000"); });

Key insight: The IP gets traffic to the right machine. The port gets it to the right program on that machine.

In short: A port tells the server which service should receive the traffic.

4. What Happens When You Open a Website? ๐ŸŒ

Let's say you visit https://example.com.

  1. DNS lookup: Your browser finds the IP address.
  2. Connection: Your device establishes a connection to the server.
  3. Port: The connection reaches the HTTPS service, typically on port 443.
  4. Request: Your browser sends an HTTP request.
  5. Response: The server sends back a response.

All of this usually happens in a fraction of a second.

In short: Find the IP, connect to the right port, send a request, get a response.

5. Localhost vs Public IP ๐Ÿ 

When developing a backend, you will often see:

text
http://localhost:3000
  • localhost refers to your own device. It usually points to the loopback address 127.0.0.1 (or ::1 in IPv6).
  • 3000 is the port where your application is listening.

A public IP can make a server reachable over the internet, subject to network and firewall configuration.

AddressWho can usually reach itExample
localhostOnly your own machinehttp://localhost:3000
Private IPDevices on the same local network192.168.1.10:3000
Public IPAnyone on the internet, if the network and firewall allow it142.250.190.14

Common mistakes

  • Opening localhost:3000 on your phone to reach the server on your laptop. On your phone, localhost means the phone itself. Use your laptop's local network IP instead (if your firewall and server settings allow it).
  • Forgetting the port. http://localhost goes to port 80, not to your app on port 3000.
  • Two apps on one port. Only one program can listen on a given port at a time. If you see an error like EADDRINUSE, something is already using that port.
  • Thinking a domain is the server. The domain is just a name. DNS points it to an IP, and the server lives at that IP.

6. The Big Picture ๐Ÿง 

ConceptThe question it answers
๐Ÿ“ IPWhere is the destination?
๐Ÿ”Ž DNSWhich IP belongs to this domain?
๐Ÿšช PortWhich service should receive the traffic?
๐Ÿ  localhost"This same machine"

That's the foundation of how your backend becomes reachable over a network.

๐Ÿงช Try it yourself

  1. Do a DNS lookup in your terminal and see the IP addresses behind a domain:

    bash
    nslookup youtube.com

    You may see several IPs. Large sites use many servers.

  2. Start the Express server above, then open http://localhost:3000 in your browser. Now try http://localhost:3001. Nothing is listening there, so the connection fails.

  3. Watch a full request with curl. The -v flag shows the connection to the IP and port, then the request and response:

    bash
    curl -v https://example.com

๐ŸŽฏ Day 2 in one line

DNS turns a domain name into an IP address, the IP address tells you where the server is, and the port tells you which service on that server should receive your request.

IP AddressDNSPortsLocalhostPublic IPRequest Flow
Next chapterDay 3: Frontend vs Backend and Why You Never Trust the Client

Chapter 3 ยท Oct 23, 2026

Day 3: Frontend vs Backend and Why You Never Trust the Client

backend-seriesbasicssecurity

Key takeaway

The client sends only IDs and quantities; the backend looks up prices, checks permissions and verifies identity on every request.

1. The Shop With Self-Written Price Tags ๐Ÿท๏ธ

Imagine a shop where customers write the price on an item themselves, and the cashier charges whatever the tag says.

Someone picks up a laptop, writes โ‚น1, and walks away with it.

Absurd? That's exactly what happens when a backend trusts the price sent by the frontend.

In short: The backend must check the real price, never trust the customer's tag.

2. What the Frontend Is Responsible For ๐ŸŽจ

The frontend runs on the user's device. Its job is to provide the experience:

  • Layout, design and animations
  • Clicks, forms and navigation
  • Instant feedback, like "Invalid email"
  • Managing local UI state (is this menu open? which tab is selected?)
  • Sending requests to the backend and displaying the results

Frontend validation improves usability, but it does not guarantee security. It's there to help honest users, not to stop dishonest ones.

In short: The frontend makes things convenient. It cannot enforce security on its own.

3. What the Backend Is Responsible For โš™๏ธ

The backend runs on a server you control. Its job is correctness, security and business logic:

ResponsibilityWhat it meansExample
ValidationReject invalid dataQuantity must be a positive number
AuthenticationEstablish who the user isVerify the session or token
AuthorizationDecide what the user can accessOnly admins can delete products
Business rulesCalculate prices, apply discounts, check stockTotal = price ร— quantity โˆ’ discount
Data accessRead and update the databaseSave the order
SecretsProtect API keys and database credentialsThe payment gateway secret key
ProtectionApply rate limits and record relevant eventsMax 5 login attempts per minute

In short: The backend enforces the rules that matter.

4. Why You Can Never Trust the Client ๐Ÿšซ

Everything running on a user's device is under their control:

  • Browser DevTools can inspect frontend code.
  • The Network tab exposes every request and response.
  • Requests can be replayed or modified using external tools.
  • Anyone can call a publicly reachable API without using your frontend at all.

An attacker does not need to use your nice checkout form. They can send a request directly to your server.

This line between the untrusted client and your server is called the trust boundary. Every request that crosses it must be treated as untrusted input.

Key insight: Your backend can't tell whether a request came from your beautiful UI or from someone typing it by hand. So it must check everything, every time.

In short: Never assume a request is safe just because your frontend generated it.

5. Four Things You Must Never Trust the Client For ๐Ÿ›‘

A. Validation

The frontend checks whether an email looks valid. The backend validates it again before processing the request. Client-side checks can be bypassed, so security must not depend on them.

B. Prices and money

Never trust a price or discount supplied by the client.

The client should send the product ID and quantity. The backend retrieves the authoritative price and calculates the total.

C. Permissions

Hiding an Admin button does not prevent someone from calling an admin API directly. The backend must check permissions on every relevant request and deny access by default.

D. Identity

A request claiming userId: 42 or role: "admin" proves nothing.

The backend must verify the user's session or token and establish their identity and permissions using trusted server-side data.

In short: Validate data, calculate money, enforce permissions and establish identity on the server.

6. Secrets Never Belong in the Frontend ๐Ÿ”‘

Frontend code is delivered to the user's device, so anything embedded in it can potentially be inspected.

For example, Vite exposes variables prefixed with VITE_ to client-side code:

bash
# .env in a Vite project VITE_API_URL=https://api.myshop.com # โœ… fine, this is public anyway VITE_STRIPE_SECRET=sk_live_... # โŒ shipped to every visitor's browser

Never put database passwords, private API keys or signing secrets in those variables. If the frontend needs a secret operation, it should call your backend, which uses the secret securely.

In short: If a secret reaches the browser, treat it as exposed.

7. Who Decides What? โš–๏ธ

ConcernFrontendBackend
ValidationGives instant feedbackValidates before processing
PricesDisplays themCalculates the actual total
PermissionsShows or hides buttonsEnforces access on every request
IdentityDisplays the logged-in userVerifies who they are
SecretsNever exposes private keysProtects and uses them

The rule: The frontend can guide the user, but only the backend can enforce trusted decisions.

8. What Breaks Without This? ๐Ÿ’ฅ

Imagine your checkout request contains:

json
{ "productId": "laptop123", "quantity": 1, "price": 1299.99 }

A user opens DevTools, changes the price to 1 and resends the request. If the backend blindly trusts that value, the order could be processed at the wrong price.

The correct approach:

  1. Receive only the product ID and quantity.
  2. Retrieve the current authoritative price from trusted server-side data.
  3. Validate the quantity and check stock.
  4. Calculate the total on the server.
  5. Process the order using the validated values.

Here's what that looks like in Express:

js
app.post("/checkout", requireLogin, async (req, res) => { const { productId, quantity } = req.body; // ignore any "price" the client sends if (!Number.isInteger(quantity) || quantity < 1) { return res.status(400).json({ error: "Invalid quantity" }); } const product = await Product.findById(productId); // trusted price if (!product || product.stock < quantity) { return res.status(409).json({ error: "Not available" }); } const total = product.price * quantity; // calculated on the server const order = await Order.create({ userId: req.user.id, productId, quantity, total }); res.status(201).json(order); });

The same principle applies to discounts, permissions and user identity.

Common mistakes

  • Sending price, discount or isAdmin from the frontend and using it as-is.
  • Checking permissions only in the UI ("the button is hidden, so it's safe").
  • Reading userId from the request body instead of from the verified session or token.

๐Ÿงช Try it yourself

  1. Open any shop or form on a site you own, press F12 โ†’ Network, and submit something.
  2. Right-click the request โ†’ Copy as cURL. Paste it in a terminal and change a value before sending.
  3. Notice that the server can't tell the difference. That's why every check must live on the backend.

๐ŸŽฏ Day 3 in one line

The frontend provides the experience. The backend enforces the rules. Never trust the client to make decisions that affect security, money or data.

Trust BoundaryServer-Side ValidationAuthentication vs AuthorizationBusiness LogicSecrets Management
Next chapterDay 4: How Frontend and Backend Communicate (HTTP, Polling, SSE, WebSockets, Webhooks, gRPC)

Chapter 4 ยท Oct 24, 2026

Day 4: How Frontend and Backend Communicate (HTTP, Polling, SSE, WebSockets, Webhooks, gRPC)

backend-serieshttpreal-time

Key takeaway

Start with plain HTTP. Use SSE for server-to-client streams, WebSockets for two-way real-time chat, and verified, duplicate-safe webhooks for events from other services, such as payment confirmations that must not depend on the browser.

How does a chat message appear instantly? How does Razorpay notify your backend that a payment succeeded?

Different problems need different ways to communicate. Let's understand them.

1. The Ways to Communicate ๐Ÿ—ฃ๏ธ

Think of communication like everyday life:

MethodEveryday analogy
HTTPOrdering at a counter. Ask, get an answer.
PollingAsking repeatedly, "Is it ready yet?"
Long polling"Stay on the line. I'll tell you when it's ready."
SSEListening to a radio. The server sends updates.
WebSocketsA phone call. Both sides can talk anytime.
WebhooksA doorbell. Another service notifies your server when something happens.
gRPCTwo departments using a shared, efficient language.

Each solves a different communication problem.

In short: There is no universal best method. Choose based on who needs to communicate and how frequently.

2. HTTP: The Default ๐Ÿงพ

Most web interactions follow a simple pattern:

  1. The client sends a request.
  2. The server processes it.
  3. The server returns a response.

Example: You open a product page, and your backend returns the product details.

The exchange is straightforward, but the server cannot use an ordinary completed HTTP request to spontaneously deliver a new message to the browser.

What if the client needs updates without refreshing? That's where other techniques help.

In short: HTTP request and response is the foundation of most web communication.

3. Polling vs Long Polling ๐Ÿ”

Imagine tracking a food delivery order.

Polling: Your app asks the server every 2 seconds, "Where is my order?"

js
setInterval(async () => { const res = await fetch("/api/orders/123/status"); const { status } = await res.json(); console.log("Order status:", status); }, 2000);

If 1,000 clients do this, they can generate around 500 requests per second, even when nothing changes.

Long polling: Your app asks once, and the server holds the request until an update is available or a timeout occurs. The client then makes another request.

This reduces repeated empty responses, but holding many requests open still consumes server resources.

Use cases: Basic live dashboards, delivery tracking, or environments where persistent connections are unavailable.

In short: Polling repeatedly asks. Long polling waits for an answer before asking again.

4. SSE: The Radio ๐Ÿ“ป

Server-Sent Events (SSE) allow a server to send a continuous stream of events to a browser over HTTP.

The client opens a connection. The server sends updates whenever something changes.

  • Direction: Server to client
  • Supports automatic reconnection in browsers using EventSource
  • Best for notifications, live feeds, progress updates and AI text streaming

Example: An AI response appears word by word as the server generates it.

js
// Browser const events = new EventSource("/api/notifications/stream"); events.onmessage = (event) => console.log("Update:", event.data); // Server (Express) app.get("/api/notifications/stream", (req, res) => { res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.flushHeaders(); const timer = setInterval(() => { res.write(`data: ${new Date().toISOString()}\n\n`); }, 1000); req.on("close", () => clearInterval(timer)); });

The client can still send separate HTTP requests, but the SSE connection itself is one-way.

In short: Use SSE when the server needs to push updates and the client rarely needs to send messages through the same connection.

5. WebSockets: The Phone Call ๐Ÿ“ž

WebSockets provide a persistent, two-way communication channel. After the connection is established, both client and server can send messages independently.

  • Direction: Both ways
  • Low overhead for repeated messages
  • Supports text and binary data
  • Requires connection management and reconnection logic

Example: In a chat app, you send a message and receive incoming messages without repeatedly requesting updates.

js
const socket = new WebSocket("wss://chat.example.com"); socket.addEventListener("open", () => socket.send("Hello!")); socket.addEventListener("message", (event) => { console.log("New message:", event.data); });
SSEWebSockets
DirectionServer to clientBoth ways
ReconnectionAutomatic with EventSourceYou write it yourself
DataTextText and binary
Great forFeeds, notifications, AI streamingChat, multiplayer games, collaboration

In short: Use WebSockets when both sides need frequent, real-time communication.

6. Webhooks: The Doorbell ๐Ÿ””

Webhooks are different. They typically let one service notify another server about an event.

Example: A customer pays through Razorpay.

  1. Your backend creates a payment order.
  2. The payment provider processes the payment.
  3. The provider sends an event to your webhook URL.
  4. Your backend verifies the webhook signature.
  5. Your backend safely updates the order status.

Even if the customer closes the browser, the provider can still notify your server.

Important: Webhooks can be retried or delivered more than once. Your handler must verify authenticity and safely handle duplicate events. Processing the same event twice must be harmless.

In short: Webhooks let services notify your backend when something happens, without your backend repeatedly asking.

7. gRPC: Service to Service โš™๏ธ

gRPC is a framework commonly used for communication between backend services.

It uses Protocol Buffers by default to define structured messages and serialize them efficiently. It commonly runs over HTTP/2 and supports streaming.

Example: An order service communicates with an inventory service to check stock. Both sides share a .proto file that defines exactly what a stock request and reply look like.

gRPC is particularly useful when services need strongly defined contracts and efficient communication. Browsers generally need a compatible intermediary such as gRPC-Web.

In short: gRPC is a strong option for structured, efficient communication between backend services.

8. Which One Should You Use? ๐Ÿงญ

What you needUse
Normal API calls and form submissionsHTTP
Repeated checks for updatesPolling
Waiting for updates over ordinary HTTPLong polling
Server-to-client live updatesSSE
Two-way real-time communicationWebSockets
Notifications between external servicesWebhooks
Efficient backend-to-backend communicationgRPC

Key insight: Start with ordinary HTTP. Add another communication method only when your requirements justify it.

Common mistakes

  • Using WebSockets for everything. This is the most common beginner mistake. A product page does not need a live connection.
  • Skipping webhook signature checks. Anyone can send a POST to a public URL.
  • Not handling duplicate webhooks. The same event can arrive twice, and must not ship two orders.

9. What Breaks Without the Right Approach? ๐Ÿ’ฅ

Imagine a chat app built only with normal HTTP requests. A new message might not appear until the user refreshes or requests updates again.

Or imagine a payment system that relies on the browser to tell the backend that payment succeeded. The customer closes the tab, and the notification never arrives.

The solution is to choose the correct communication pattern. For payment confirmation, a verified webhook can notify your backend independently of the browser.

๐Ÿงช Try it yourself

  1. Stream SSE in the terminal. Run the SSE server above, then:

    bash
    curl -N http://localhost:3000/api/notifications/stream

    -N turns off buffering, so events appear live.

  2. Spot a WebSocket. On a web app with live chat, filter the Network tab to WebSocket connections and click one to see its messages.

๐ŸŽฏ Day 4 in one line

HTTP handles request and response. Polling and streaming techniques deliver updates. WebSockets enable two-way real-time communication. Webhooks notify servers about events. gRPC connects services efficiently.

HTTP Request-ResponsePolling and Long PollingServer-Sent EventsWebSocketsWebhooksgRPC
Next chapterDay 5: Monolith vs Microservices

Chapter 5 ยท Oct 25, 2026

Day 5: Monolith vs Microservices

backend-seriesarchitecture

Key takeaway

A monolith is one deployable app; microservices are independently deployable services that talk over a network and bring distributed-system complexity. Start with a well-organized modular monolith and split only for a concrete reason.

Imagine you're building an e-commerce application.

Should everything live in one application, or should you split it into multiple independent services?

Let's understand the trade-offs.

1. The Single Kitchen: Monolith ๐Ÿณ

Imagine a restaurant where one kitchen handles everything: orders, cooking, billing and inventory.

A monolithic backend works similarly. Different features live inside one application, which is built and deployed as a unit.

For example, your e-commerce backend might contain:

  • Authentication
  • Products
  • Cart
  • Orders
  • Payments

All these modules run as part of the same application.

In short: One application, multiple responsibilities.

2. Separate Kitchens: Microservices ๐Ÿช

Now imagine a restaurant chain where separate teams handle billing, inventory and orders through independently operated systems.

With microservices, an application is split into smaller services. Each service owns a specific business capability and can be developed and deployed independently.

An e-commerce system might have:

  • Auth Service
  • Product Service
  • Order Service
  • Payment Service
  • Notification Service

These services communicate over a network, and each can have its own codebase and data ownership.

In short: Multiple independent services, each responsible for a specific capability.

3. What's the Actual Difference? โš–๏ธ

MonolithMicroservices
DeploymentOne application to build and deployServices can be deployed independently
DevelopmentSimpler local development and debuggingMonitoring, testing and operations become more complex
CommunicationInternal function calls can connect modules directlyNetwork communication introduces latency and failure possibilities
Release riskA deployment can affect the entire applicationA release usually touches one service
TeamsEveryone works in one codebaseTeams can work on different services
ScalingRun more copies of the whole appIndividual services can scale according to demand
Main dangerPoorly separated modules can become difficult to maintainDistributed-system complexity

The communication row is the big one. Compare:

js
// Monolith: a direct function call inside the same process const stock = await inventory.checkStock(productId);
js
// Microservices: a network request that can be slow or fail const res = await fetch(`http://inventory-service/stock/${productId}`, { signal: AbortSignal.timeout(2000), // give up after 2 seconds }); if (!res.ok) throw new Error("Inventory service unavailable"); const stock = await res.json();

The second version must handle timeouts, errors and a service that is down. The first one doesn't.

In short: Microservices provide flexibility, but they also introduce distributed-system complexity.

4. What Happens When Traffic Increases? ๐Ÿ“ˆ

Suppose your e-commerce application receives 100,000 requests, and most of them are for product browsing.

  • In a monolith, you can run multiple instances of the application, but each instance generally contains all its modules. You may end up scaling more of the application than necessary.
  • With microservices, you could scale the Product Service independently from the Order Service.

For example:

MonolithMicroservices
To handle extra browsing5 copies of the whole app5 copies of the Product Service
Order handlingComes along in every copyStill 1 copy of the Order Service

But there's a catch: independent scaling is useful only when the architecture and workload justify the extra complexity.

In short: Microservices can scale individual capabilities independently. A monolith can also scale by running multiple instances.

5. What Happens When Something Fails? ๐Ÿงฏ

Imagine the Notification Service goes down while a customer places an order.

In a well-designed microservices architecture, the order may still succeed even if sending the notification fails.

However, this isolation does not happen automatically. Services must be designed to handle failures, timeouts and retries correctly. In code, that means deciding which steps are critical and which are not:

js
const order = await saveOrder(input); // critical: must succeed try { await sendConfirmation(order); // not critical } catch (err) { console.error("Notification failed, will retry later:", err.message); } res.status(201).json(order);

In a monolith, a notification-related bug could also affect the application if it disrupts shared resources or crashes the process. But a well-designed monolith can isolate failures too.

In short: Architecture alone does not guarantee reliability. How you design failure handling matters.

6. When Should You Choose Each? ๐Ÿงญ

Choose a monolith when:

  • You're building your first product or startup
  • The team is small
  • Requirements are still changing
  • You want simpler development and deployment
  • You don't yet have a clear reason to split services

Consider microservices when:

  • Different business capabilities need independent scaling
  • Multiple teams need independent ownership and releases
  • The system has clear service boundaries
  • You can support distributed tracing, monitoring and failure handling
  • The benefits outweigh the operational cost

In short: Start simple. Split services when you have a concrete reason, not just because the architecture looks impressive.

7. Monolith Doesn't Mean Messy ๐Ÿงฉ

A common misconception: a monolith does not have to mean messy code.

You can build a modular monolith, where authentication, products, orders and payments are separated into well-defined modules but deployed as one application.

text
shop-backend/ src/ auth/ products/ cart/ orders/ payments/ server.js โ† one app, one deployment

Each module exposes a small public interface, and other modules use only that:

js
// orders/index.js: the only door into the orders module export { createOrder, getOrder } from "./orderService.js";

This gives you simpler operations while keeping the code organized. If a module later needs to become an independent service, a clear boundary makes that transition easier.

In short: Good architecture starts with clear boundaries, not with more servers.

Common mistakes

  • Starting with microservices for a small app "because big companies do it".
  • Splitting without clear boundaries. Services that must call each other for every request give you the costs of both designs.
  • Assuming microservices are automatically more reliable. Without timeouts and retries, one slow service can drag down others.
  • Letting a monolith turn into a ball of mud. Any module reaching into any other module's internals makes change painful.

๐Ÿงช Try it yourself

Feel the difference between a function call and a network call:

  1. Save this as inventory.js and run it with node inventory.js (in a project with Express installed and "type": "module" in package.json):

    js
    import express from "express"; const app = express(); app.get("/stock/:id", (req, res) => res.json({ id: req.params.id, available: 5 })); app.listen(3001, () => console.log("Inventory service on port 3001"));
  2. Call your tiny "service" from another terminal:

    bash
    curl http://localhost:3001/stock/42
  3. Stop the service with Ctrl+C and run the same curl again. You get a connection error. That is the kind of failure every microservice has to plan for, and a direct function call inside a monolith never sees.

๐ŸŽฏ Day 5 in one line

A monolith is one deployable application. Microservices are independently deployable services. Start with the simplest architecture that meets your needs.

Phase 1 complete! ๐ŸŽฏ You now know how the web fits together: clients and servers, how requests find a server, who decides what, how frontends and backends communicate, and how a backend can be structured. Phase 2 begins next.

MonolithMicroservicesModular MonolithIndependent ScalingFailure IsolationService Boundaries
Next chapterDay 6: HTTP Methods, URLs, Path and Query Parameters

Chapter 6 ยท Oct 26, 2026

Day 6: HTTP Methods, URLs, Path and Query Parameters

backend-serieshttprest-api

Key takeaway

The method says what action to take, the path says which resource, path parameters pick a specific resource, and query parameters filter, sort or paginate. Both arrive as strings, so validate them and check access.

You call an API, but how does the backend know what resource you want and what action you're trying to perform?

Let's break down the basics.

1. Anatomy of a URL ๐Ÿงญ

Consider this API URL:

text
https://api.shop.com/products/42?currency=INR

Here's what each part means:

PartValueMeaning
SchemehttpsHow to talk to the server (secure HTTP)
Hostapi.shop.comWhich server to send the request to
Path/products/42Which resource on that server
Query parameter?currency=INRExtra details about what you want

Notice there is no port in this URL. As you saw in Day 2, https uses port 443 by default.

The URL tells the client where to send the request and identifies the resource or information being requested.

In short: A URL identifies the destination and the requested resource.

2. HTTP Methods: What Action Do You Want? ๐Ÿ› ๏ธ

The same resource can support different operations. Imagine /products/42:

MethodWhat it doesExample
GETRetrieves the resourceGET /products/42 โ†’ fetch product 42
POSTSubmits data, commonly to create a resourcePOST /products โ†’ create a new product
PUTReplaces a resource's representationPUT /products/42 โ†’ replace product 42 with the data sent
PATCHPartially updates a resourcePATCH /products/42 โ†’ update some fields of product 42
DELETERequests deletion of a resourceDELETE /products/42 โ†’ delete product 42

Here is what a real PATCH request looks like on the wire:

http
PATCH /products/42 HTTP/1.1 Host: api.shop.com Content-Type: application/json { "price": 1199 }

Only the price is sent. With PUT, you would send the whole product, because PUT replaces it.

The method and path work together to describe the operation. In Express, each method gets its own route:

js
app.get("/products/:id", getProduct); app.patch("/products/:id", updateProduct); app.delete("/products/:id", deleteProduct);

Key insight: Same path, different method, completely different action.

In short: The path identifies the resource. The method describes the intended operation.

3. Path Parameters: Which Specific Resource? ๐ŸŽฏ

Path parameters identify a particular resource or a position within a resource hierarchy.

http
GET /users/17

Fetch user 17.

http
GET /users/17/orders/92

Fetch order 92 belonging to user 17.

In Express, you can access them using req.params. You define the route with a placeholder:

text
/products/:productId

For /products/42, the backend receives:

js
req.params.productId; // "42" (a string, not a number)

Remember, a path parameter is just input. The backend must still validate it and check whether the user is allowed to access that resource.

js
app.get("/products/:productId", async (req, res) => { const id = Number(req.params.productId); if (!Number.isInteger(id) || id < 1) { return res.status(400).json({ error: "Invalid product ID" }); } const product = await findProduct(id); if (!product) { return res.status(404).json({ error: "Product not found" }); } res.json(product); });

In short: Path parameters identify which resource the request refers to.

4. Query Parameters: How Should Results Be Selected? ๐Ÿ”

Query parameters commonly control filtering, searching, sorting and pagination. They come after the ?, and each pair is separated by &.

http
GET /products?category=phones&sort=price&page=2

This could mean:

  • Show products in the phones category.
  • Sort by price.
  • Return page 2.

In Express, access them through req.query:

js
req.query.category; // "phones" req.query.page; // "2"

Query values usually arrive as strings, so validate and convert them when necessary. A query value can even be missing, or arrive as an array if the same key is repeated.

js
app.get("/products", (req, res) => { const page = Number(req.query.page ?? 1); // "2" โ†’ 2, missing โ†’ 1 if (!Number.isInteger(page) || page < 1) { return res.status(400).json({ error: "page must be a positive whole number" }); } // ...find products for this page });
Path parameterQuery parameter
Looks like/products/42/products?page=2
AnswersWhich resource?Which results and how?
Typical useIDs, nested resourcesFilters, search, sorting, pagination
In Expressreq.paramsreq.query

In short: Path parameters identify a resource. Query parameters commonly control which results you want and how they are returned.

5. Can We Use Both Together? ๐Ÿค

Absolutely. Consider:

http
GET /users/17/orders?status=delivered&page=2
PartWhat it does
users/17Identifies the user
ordersIdentifies the resource collection
status=deliveredFilters the orders
page=2Selects a page of results
js
app.get("/users/:userId/orders", (req, res) => { const { userId } = req.params; // "17" const { status, page } = req.query; // "delivered", "2" // validate, check access, then query the database });

One request, multiple pieces of information.

6. A Common Beginner Mistake โš ๏ธ

These two URLs are not equivalent:

http
GET /products/42 GET /products?id=42

The first uses a path parameter. The second uses a query parameter.

Both designs can be valid, but your backend must define and implement the expected route. In Express, routes match on the path only, so /products?id=42 is handled by app.get("/products"), not by app.get("/products/:id").

Also, never assume that a valid-looking ID grants access to the corresponding resource. Changing /users/17/orders to /users/18/orders must not show someone else's orders.

Common mistakes

  • Treating params as numbers. req.params.id and req.query.page are strings. "2" + 1 is "21", not 3.
  • Skipping validation because "the frontend only sends valid IDs". Anyone can type any URL (remember Day 3).
  • Skipping the access check. A valid ID only says which resource, not who is allowed to see it.
  • Using GET to change data. GET should retrieve. Use POST, PUT, PATCH or DELETE for changes.
  • Mixing up PUT and PATCH. PUT replaces the whole thing. PATCH updates only the fields you send.

๐Ÿงช Try it yourself

JSONPlaceholder is a free fake API for practice. It pretends to save changes but never actually does.

  1. Path parameter, fetch one post:

    bash
    curl https://jsonplaceholder.typicode.com/posts/1
  2. Query parameter, filter posts by user:

    bash
    curl "https://jsonplaceholder.typicode.com/posts?userId=1"
  3. A different method on the same path:

    bash
    curl -X PATCH https://jsonplaceholder.typicode.com/posts/1 \ -H "Content-Type: application/json" \ -d '{"title":"Updated title"}'

Notice the quotes around the URL in step 2. Without them, your shell may treat ? or & as special characters.

๐ŸŽฏ Day 6 in one line

HTTP methods describe the intended action, paths identify resources, path parameters specify particular resources, and query parameters refine the request.

URL AnatomyHTTP MethodsPUT vs PATCHPath ParametersQuery ParametersInput Validation
Next chapterDay 7: HTTP Headers, Request Body and Content Types

Chapter 7 ยท Oct 27, 2026

Day 7: HTTP Headers, Request Body and Content Types

backend-serieshttp

Key takeaway

Headers are labels that describe the message, the body carries the data, and Content-Type must match the body's real format. Accept says which response formats the client can handle. Always validate the body, even when it parses correctly.

When your frontend sends data to a backend, how does the server know what the data means and how to interpret it?

Let's break it down.

1. HTTP Headers: Labels on a Package ๐Ÿท๏ธ

Headers carry metadata about an HTTP request or response.

Think of them as a label attached to a package. They describe the package, but they aren't the contents inside it.

Common headers

HeaderWhat it tells the receiverExample value
Content-TypeFormat of the message bodyapplication/json
AcceptResponse formats the client can handleapplication/json
AuthorizationCredentials used to authenticate a requestBearer <token>
Cache-ControlInstructions related to cachingno-cache

Headers can appear in both requests and responses.

Example:

http
Content-Type: application/json

This tells the receiver that the body contains JSON data.

What a full request looks like

Here is a raw HTTP request with its headers and body. A blank line separates the headers from the body:

http
POST /users HTTP/1.1 Host: api.example.com Content-Type: application/json Accept: application/json {"name": "D3", "email": "d3@example.com"}

Header names are case-insensitive, so content-type and Content-Type mean the same thing.

In short: Headers describe a message and provide information needed to interpret or handle it.

2. Request Body: The Actual Data ๐Ÿ“ฆ

When creating a user, placing an order, or updating a profile, the client often sends data in the request body.

Example:

http
POST /users Content-Type: application/json { "name": "D3", "email": "d3@example.com" }

The backend receives this data and processes it according to the application's rules.

Not every request needs a body. A typical GET request retrieves data without one.

MethodUsually has a body?Example
GETNoFetch a product
POSTYesCreate a user
PUT / PATCHYesUpdate a profile
DELETEUsually noRemove a cart item

In Express, you turn on a body parser so the JSON body appears on req.body:

js
import express from "express"; const app = express(); app.use(express.json()); // parses bodies sent as application/json app.post("/users", (req, res) => { const { name, email } = req.body; // Validate before using this data! res.status(201).json({ name, email }); });

In short: The request body carries the data being submitted to the server.

3. Content-Type: What Format Is the Body In? ๐Ÿ“„

The same information can be represented in different formats.

JSON

http
Content-Type: application/json { "productId": "p123", "quantity": 2 }

URL-encoded form data

http
Content-Type: application/x-www-form-urlencoded productId=p123&quantity=2

Multipart form data

http
Content-Type: multipart/form-data

Commonly used when submitting form fields along with files, such as a profile picture. In a real request, this header also includes a boundary=... value that separates the parts. Browsers and HTTP libraries add it for you.

Comparing the three

Content-TypeBody looks likeTypical useExpress parser
application/json{"quantity": 2}APIs and fetch callsexpress.json()
application/x-www-form-urlencodedproductId=p123&quantity=2Classic HTML formsexpress.urlencoded()
multipart/form-dataSeveral parts split by a boundaryFile uploads with fieldsA library such as multer

These formats aren't interchangeable. The server needs to interpret the body according to its actual format.

In short: Content-Type tells the receiver how the message body is represented.

4. When the Content-Type Is Wrong โŒ

Imagine sending this body, but declaring the wrong type:

http
POST /cart Content-Type: text/plain { "quantity": 2 }

The server may treat the body as plain text instead of parsing it as JSON.

Depending on the server's implementation, the expected data may not be available in the format the application needs, or the request may be rejected. For example, express.json() only parses JSON content types, so req.body.quantity will not be there. Some APIs reject the request with 415 Unsupported Media Type.

In short: The body format and Content-Type must agree, and the server must support that format.

5. Content-Type vs Accept ๐Ÿ”„

These two headers are easy to confuse.

HeaderQuestion it answersIt is about
Content-TypeWhat format is this message body in?The data being sent
AcceptWhat response formats can the client handle?The response the client wants back

Example:

http
POST /users Content-Type: application/json Accept: application/json

The client sends JSON and indicates that it can accept a JSON response.

Content-Type also appears on responses. When the server replies, it uses Content-Type to describe the body it is sending back.

Key insight: One describes the data being sent. The other expresses a preference for the response format.

6. Putting It All Together ๐Ÿ›’

Imagine adding a product to your cart.

Request:

http
POST /cart Content-Type: application/json Accept: application/json { "productId": "p123", "quantity": 2 }

Here's what each part contributes:

PartWhat it contributes
Method and URLIdentify the operation and destination
HeadersDescribe the message and its expected handling
BodyCarries the product ID and quantity

This is how the frontend would send that request with fetch:

js
const res = await fetch("/cart", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ productId: "p123", quantity: 2 }), });

Key insight: The backend must still validate the incoming data. Correct formatting does not make the data trustworthy.

Common mistakes

  • Passing a plain object as the fetch body without JSON.stringify(). The browser sends the text [object Object] instead of JSON.
  • Declaring Content-Type: application/json but sending form data, or the other way round.
  • Setting Content-Type by hand when uploading FormData. This drops the boundary, so let the browser set it.
  • Forgetting the body parser (express.json()), then wondering why req.body is empty.
  • Mixing up Content-Type (what I'm sending) and Accept (what I want back).
  • Trusting the body just because it parsed correctly.

๐Ÿงช Try it yourself

  1. Look at real response headers. The -i flag shows them above the body:

    bash
    curl -i https://example.com

    Find the Content-Type line. For a web page, it will be something like text/html.

  2. Send JSON to an echo service that shows what it received:

    bash
    curl -X POST https://httpbin.org/post \ -H "Content-Type: application/json" \ -d '{"productId": "p123", "quantity": 2}'

    The json field in the reply shows your parsed body. Now remove the -H line and run it again. Without it, curl labels the body as application/x-www-form-urlencoded by default, so the server tries to read your JSON as form data. Compare the form and json fields in the two replies.

  3. Open DevTools, go to the Network tab, and click any request. Under Headers, compare the request headers with the response headers.

๐ŸŽฏ Day 7 in one line

Headers describe the message, the body carries the data, and Content-Type tells the receiver how to interpret it.

HTTP HeadersRequest BodyContent-TypeAccept HeaderBody FormatsInput Validation
Next chapterDay 8: HTTP Status Codes Explained

Chapter 8 ยท Oct 28, 2026

Day 8: HTTP Status Codes Explained

backend-serieshttprest-api

Key takeaway

Return the status code that describes what actually happened: 2xx success, 3xx redirection, 4xx a problem with the request (401 not authenticated, 403 not allowed), 5xx a server or upstream failure. Never return 200 for errors.

Your API returns 404, 401, or 500.

What do these numbers actually mean, and when should your backend return each one?

Let's break it down.

1. What Are HTTP Status Codes? ๐Ÿ”ข

An HTTP status code is a three-digit number sent by the server to indicate the result of a request.

It is the very first thing in a response, on the status line:

http
HTTP/1.1 404 Not Found Content-Type: application/json {"error": "Product not found"}

Status codes are grouped into five categories:

RangeCategoryWhat it means
1xxInformationalInterim information before the final response
2xxSuccessThe request worked
3xxRedirectionLook somewhere else, or reuse what you have
4xxClient errorSomething is wrong with the request or the client's permissions
5xxServer errorThe server or something it depends on failed

As a backend developer, you'll mostly work with 2xx, 4xx, and 5xx.

In short: Status codes tell the client how the server handled a request.

2. 2xx: Success โœ…

The request succeeded.

CodeNameWhen to useExample
200OKThe request succeededGET /products/42
201CreatedA resource was createdPOST /users
202AcceptedThe request was accepted for processing, but processing may not be completePOST /reports that starts a background job
204No ContentThe request succeeded, and there is no response bodyDELETE /cart/items/42

A successful request does not always mean the server returns data. 204 is a good example.

In Express, you set the code with res.status():

js
app.post("/users", async (req, res) => { const user = await createUser(req.body); res.status(201).json(user); // a user was created }); app.delete("/cart/items/:id", async (req, res) => { await removeItem(req.params.id); res.status(204).end(); // success, nothing to send back });

3. 3xx: Redirection ๐Ÿ”

The client needs to follow a redirect or use a different location.

CodeNameWhen to useExample
301Moved PermanentlyThe resource has permanently movedAn old blog URL now lives at a new address
302FoundA temporary redirectSend the user to a login page for now
304Not ModifiedThe cached resource is still valid, so the client can reuse itBrowser re-checks a cached image that hasn't changed

Example: A browser requests an old URL, and the server redirects it to a new URL. The new address goes in the Location header:

http
HTTP/1.1 301 Moved Permanently Location: https://example.com/new-page

In short: Redirection helps clients find resources or reuse cached responses.

4. 4xx: Client Errors โš ๏ธ

The request cannot be fulfilled because of something about the request or the client's permissions.

CodeNameWhen to useExample
400Bad RequestThe request is malformed or invalidInvalid JSON
401UnauthorizedValid authentication is missing or has failedMissing or invalid login credentials
403ForbiddenThe server refuses accessA user without permission tries to access an admin resource
404Not FoundThe requested resource was not foundA product ID does not exist
409ConflictThe request conflicts with the current state of the resourceAn email is already registered
422Unprocessable ContentThe content is understood, but its instructions cannot be processedValid JSON, but the values break your rules
429Too Many RequestsThe client has sent too many requestsToo many requests in a short period

401 vs 403: the classic mix-up

401 Unauthorized403 Forbidden
The server is saying"I don't know who you are.""I know who you are, but you can't do this."
Typical causeNo token, expired session, wrong passwordLogged in, but not an admin

Think of an office building. No badge at the door is a 401. A valid badge that doesn't open the server room is a 403.

In short: 4xx codes indicate that the request cannot be fulfilled as submitted. The exact code depends on the situation.

5. 5xx: Server Errors ๐Ÿ”ฅ

The server failed to fulfil an otherwise valid request.

CodeNameWhen it happensExample
500Internal Server ErrorAn unexpected server-side failureAn unhandled exception in your code
502Bad GatewayA gateway or proxy received an invalid response from an upstream serverYour app crashed behind a proxy such as Nginx
503Service UnavailableThe server is temporarily unable to handle the requestThe server is overloaded or down for maintenance
504Gateway TimeoutA gateway or proxy did not receive an upstream response in timeSee the diagram below

Example: Your API depends on another service, which fails to respond before the gateway's timeout. The client receives 504 Gateway Timeout.

In short: 5xx codes indicate a server-side or upstream failure, not necessarily a bug in your own application code.

6. Which Status Code Should I Return? ๐Ÿงญ

A simplified guide for an endpoint that requires login. Real APIs may order these checks differently, and any unexpected failure becomes a 500.

Two more to keep in mind: 409 Conflict when the request clashes with existing data (like a duplicate email), and 429 Too Many Requests when a client hits your rate limit.

7. Common Mistakes Backend Developers Make ๐Ÿšซ

  • Returning 200 OK for every outcome, including failures.
  • Using 401 when the user is authenticated but lacks permission. Usually, that calls for 403.
  • Returning 500 for ordinary validation errors that should be represented by an appropriate 4xx code.
  • Returning 201 Created when no resource was created.
  • Returning detailed stack traces or internal error information to clients.
js
// BAD: "success" for a failure, and internals leaked to the client res.status(200).json({ success: false, error: "User not found" }); res.status(500).json({ error: err.stack }); // GOOD: honest status codes, details logged on the server only res.status(404).json({ error: "User not found" }); console.error(err); res.status(500).json({ error: "Something went wrong" });

Choose a status code that accurately represents the outcome.

8. A Real API Example: Registration ๐Ÿ“

Imagine a registration endpoint: POST /users.

SituationStatus code
Valid details, user created201 Created
Invalid input400 Bad Request or 422 Unprocessable Content, depending on your API's conventions
Email already registered409 Conflict
Unexpected database or application failure500 Internal Server Error
js
app.post("/users", async (req, res) => { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: "Email and password are required" }); } try { const existing = await db.findUserByEmail(email); if (existing) { return res.status(409).json({ error: "Email already registered" }); } // Real apps hash the password before saving it const user = await db.createUser({ email, password }); return res.status(201).json({ id: user.id, email: user.email }); } catch (err) { console.error(err); return res.status(500).json({ error: "Something went wrong" }); } });

Key insight: The status code should describe what actually happened, not simply whether your code executed.

๐Ÿงช Try it yourself

  1. Ask a test service to return any status code you like. The status line is the first line of the output:

    bash
    curl -i https://httpbin.org/status/404 curl -i https://httpbin.org/status/201
  2. See a redirect. GitHub sends plain http:// requests to HTTPS, and Location shows where:

    bash
    curl -I http://github.com
  3. Open DevTools, go to the Network tab, and reload a page. Look at the Status column. You may spot 304 for files your browser already had cached.

๐ŸŽฏ Day 8 in one line

2xx means success, 3xx means redirection, 4xx means a client-side request problem, and 5xx means a server-side or upstream failure.

HTTP Status Codes2xx Success3xx Redirection4xx Client Errors5xx Server Errors401 vs 403
Next chapterDay 9: HTTP/1.1 vs HTTP/2 vs HTTP/3, HTTPS and TLS

Chapter 9 ยท Oct 29, 2026

Day 9: HTTP/1.1 vs HTTP/2 vs HTTP/3, HTTPS and TLS

backend-serieshttpsecurity

Key takeaway

HTTP/1.1 reuses TCP connections, HTTP/2 multiplexes streams over one TCP connection, HTTP/3 uses QUIC over UDP, and HTTPS uses TLS to encrypt traffic in transit and authenticate the server. HTTPS does not replace auth or validation.

Your browser loads a website with dozens of images, scripts, and API calls.

How does HTTP handle all those requests? And what makes HTTPS secure?

Let's break it down.

1. First, What Is HTTP? ๐ŸŒ

HTTP (Hypertext Transfer Protocol) is the protocol clients and servers use to exchange requests and responses.

Your frontend sends a request. The server processes it and returns a response.

But websites became more complex. One page could require dozens of resources, and performance became increasingly important.

That's why HTTP evolved.

VersionStandardizedRuns on
HTTP/1.11997TCP
HTTP/22015TCP
HTTP/32022QUIC over UDP

The meaning of a request like GET /products stays the same in every version. What changes is how the messages travel.

In short: HTTP defines how clients and servers communicate. Newer versions improve how that communication works.

2. HTTP/1.1: One Connection, Limited Concurrency ๐Ÿšถ

HTTP/1.1 introduced persistent connections, allowing multiple requests to reuse a TCP connection instead of opening a new one every time.

However, responses on a single connection generally need to be delivered in order. A slow response can delay later responses on that connection.

Think of a single checkout lane. If one customer takes a long time, everyone behind them waits.

Browsers often work around this by opening multiple connections to the same server (commonly around six per host).

Example: A product page requests an HTML document, several images, CSS, and JavaScript files. The browser manages these requests using a combination of persistent and multiple connections.

In short: HTTP/1.1 supports connection reuse, but handling many concurrent resources can be less efficient.

3. HTTP/2: More Efficient Communication ๐Ÿš€

HTTP/2 introduced multiplexing, allowing multiple request and response streams to share a single connection.

It also uses binary framing and compresses HTTP headers with HPACK.

FeatureWhat it means
MultiplexingMany requests and responses travel at the same time over one connection
Binary framingMessages are split into small binary frames instead of plain text lines
HPACKRepeated headers (like cookies and user agents) are compressed

Instead of needing separate connections for many concurrent requests, a browser can send multiple streams over one TCP connection.

Example: A page requests 20 product images. HTTP/2 can carry those requests concurrently over one connection.

The remaining problem: TCP head-of-line blocking

One limitation remains: HTTP/2 typically runs over TCP. If a TCP packet is lost, delivery of later data on that connection can be delayed while the missing data is recovered.

TCP delivers bytes strictly in order. So even if the lost packet belonged to image 3, data for images 4 to 20 can be held back until it is resent. All streams share that one ordered pipe.

In short: HTTP/2 improves concurrency and reduces overhead, but it still inherits TCP-level head-of-line blocking.

4. HTTP/3: Built on QUIC โšก

HTTP/3 changes the underlying transport.

Instead of TCP, it uses QUIC, which runs over UDP and provides reliable streams, encryption, and connection management.

HTTP/3 allows independent streams to make progress without one lost packet blocking all the others at the transport layer.

It can also improve connection establishment and connection migration when network conditions change.

Example: You're using a website on your phone and switch from Wi-Fi to mobile data. QUIC can support connection migration, provided the connection and network conditions permit it.

In short: HTTP/3 uses QUIC to improve transport behavior, particularly for multiplexing and changing network conditions.

5. HTTP/1.1 vs HTTP/2 vs HTTP/3 ๐Ÿ“Š

HTTP/1.1HTTP/2HTTP/3
TransportTCPTCPQUIC over UDP
ConnectionsPersistent connectionsOne connection, many streamsOne connection, many streams
ConcurrencyCommonly uses multiple connectionsMultiplexed streamsMultiplexed streams
Head-of-line blockingResponses wait in order on a connectionStill affected at the TCP levelNo TCP-level blocking between streams
Header compressionNoneHPACKQPACK
Message formatTextBinary framesBinary frames

Here is how the layers stack up:

Important: HTTP/3 is not automatically faster in every situation. Actual performance depends on the network, server, client, and workload.

6. HTTP vs HTTPS ๐Ÿ”’

HTTP defines how messages are exchanged. HTTPS is HTTP protected by TLS.

With ordinary HTTP, traffic is not encrypted by the protocol. It is like sending a postcard: anyone handling it along the way can read it.

With HTTPS, TLS provides:

ProtectionWhat it does
ConfidentialityPrevents network observers from reading protected traffic
IntegrityHelps detect unauthorized modification of traffic
AuthenticationAllows the client to verify the server's identity using its certificate and the associated trust chain

Example:

text
http://example.com -> The HTTP connection is not protected by TLS (default port 80) https://example.com -> The connection uses TLS to protect HTTP traffic (default port 443)

In short: HTTPS protects HTTP communication against eavesdropping and tampering and helps authenticate the server.

7. What Is TLS? ๐Ÿค

TLS (Transport Layer Security) is the security protocol used by HTTPS.

At a high level:

  1. The client connects to the server.
  2. They negotiate security parameters and establish shared cryptographic keys.
  3. The client verifies the server's certificate.
  4. HTTP messages are exchanged over the protected connection.

Who handles TLS?

You generally don't need to implement TLS yourself. Your web server, reverse proxy, hosting platform, or load balancer usually handles it.

When a proxy decrypts the traffic before passing it to your app, we say it terminates TLS:

text
Browser --HTTPS--> Load balancer / reverse proxy --HTTP--> Your Node.js app (TLS terminates here)

In this setup, your app may see a plain http request even though the user connected over HTTPS. In Express, you tell the app to trust the proxy's forwarded information:

js
// Trust the first proxy in front of the app, // so req.protocol and req.secure reflect the original HTTPS request app.set("trust proxy", 1);

In short: TLS creates a secure channel through which HTTP messages can travel.

8. What Should a Backend Developer Know? ๐Ÿงฐ

When building and deploying APIs:

  • Use HTTPS for production traffic.
  • Understand whether your server or reverse proxy terminates TLS.
  • Know which HTTP versions your infrastructure supports.
  • Don't assume every client supports HTTP/3.
  • Remember that HTTPS encrypts traffic in transit, not data automatically stored in your database.
  • Never treat HTTPS as a replacement for authentication, authorization, or input validation.

Key insight: HTTPS protects the road, not the destination. Once data reaches your server, your own code is responsible for checking and protecting it.

Common mistakes

  • Thinking HTTPS tells you who the user is. TLS lets the client verify the server. You still need login and permissions.
  • Loading http:// scripts or images on an https:// page (mixed content). Browsers block or warn about this.
  • "Fixing" certificate errors by turning off verification in code, for example with NODE_TLS_REJECT_UNAUTHORIZED=0. This removes the protection HTTPS gives you.

๐Ÿงช Try it yourself

  1. Ask for HTTP/2 and check what the server agrees to. The first line shows the version used:

    bash
    curl -I --http2 https://example.com

    If the server supports it, you'll see HTTP/2 200.

  2. Watch the TLS handshake. The lines starting with * describe the TLS version and the server's certificate:

    bash
    curl -v https://example.com
  3. In your browser, open DevTools, go to the Network tab, right-click a column header and enable Protocol. Reload the page and you'll see values like http/1.1, h2 or h3.

๐ŸŽฏ Day 9 in one line

HTTP/1.1 uses persistent connections, HTTP/2 adds multiplexing over TCP, HTTP/3 uses QUIC over UDP, and HTTPS uses TLS to secure HTTP communication.

HTTP/1.1HTTP/2 MultiplexingHTTP/3 and QUICHead-of-Line BlockingHTTPSTLS
Next chapterDay 10: Cookies, Sessions and CORS

Chapter 10 ยท Oct 30, 2026

Day 10: Cookies, Sessions and CORS

backend-serieshttpsecurity

Key takeaway

Cookies store small data in the browser, sessions keep state on the server behind a session ID cookie, and CORS tells browsers which origins may read responses. CORS is not authentication, and with credentials the allowed origin cannot be *.

You log in to a website, close the browser, and open it again later. Sometimes you're still logged in.

How does the server remember you between requests?

And why does your browser sometimes block an API request even when the backend is working perfectly?

Let's understand three important concepts.

1. The Problem: HTTP Is Stateless ๐Ÿง 

HTTP is a stateless protocol.

This means the server doesn't automatically remember previous requests.

Imagine:

  1. You log in to an application.
  2. The server verifies your credentials.
  3. You request your profile.
  4. The server receives another request.

How does it know that the second request belongs to the user who just logged in?

HTTP doesn't automatically maintain that relationship. We need a mechanism to maintain state across requests.

In short: HTTP treats each request independently. Cookies and sessions help applications maintain state.

2. Cookies: Small Data Stored in the Browser ๐Ÿช

A cookie is a small piece of data that a server asks the browser to store.

The server sends a Set-Cookie response header:

http
HTTP/1.1 200 OK Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax

The browser stores the cookie and automatically sends it with matching future requests:

  1. Server sends a cookie.
  2. Browser stores it.
  3. Browser makes another request to the matching domain and path.
  4. Browser includes the cookie in the request's Cookie header.
http
GET /profile HTTP/1.1 Host: example.com Cookie: sessionId=abc123
AttributeWhat it does
HttpOnlyPrevents JavaScript from reading the cookie (it won't appear in document.cookie)
SecureSends the cookie only over secure (HTTPS) connections
SameSiteControls when cookies are sent with cross-site requests: Strict, Lax or None
Max-Age / ExpiresControls cookie lifetime. Without them, the cookie usually disappears when the browser session ends

That last row explains the opening question: a cookie with a long Max-Age survives closing the browser, so you stay logged in.

In Express, setting a cookie looks like this:

js
res.cookie("sessionId", "abc123", { httpOnly: true, secure: true, sameSite: "lax", maxAge: 24 * 60 * 60 * 1000, // 1 day, in milliseconds });

In short: Cookies allow browsers to store and automatically send small pieces of data with requests.

3. Sessions: How the Server Remembers You ๐Ÿ—‚๏ธ

A session is a way for the server to maintain information about a user's interaction across multiple requests.

A common session-based approach works like this:

Example:

Browser cookie:

text
sessionId=abc123

Server-side session data:

json
{ "userId": "user42", "role": "admin", "loggedIn": true }

The browser holds the session ID. The actual session data is maintained on the server.

The session store could be memory, Redis or a database. Memory is fine for learning, but it is wiped when the server restarts and isn't shared between multiple servers.

In short: The cookie carries the session ID. The server uses that ID to find the user's session.

4. Cookies vs Sessions ๐Ÿจ

These are related, but they aren't the same thing.

CookiesSessions
Lives inThe browserThe server (memory, Redis or a database)
How it travelsAutomatically sent with matching requestsCommonly identified using a session ID, usually in a cookie
What it holdsDifferent kinds of small dataUser-related information
ControlAttributes control their behaviorCan be invalidated on the server

Think of a hotel: the cookie is your room key card. The session is your actual booking information stored in the hotel's system.

Because the session lives on the server, logging out can truly end it. The server deletes the session, and the old session ID becomes useless.

In short: Cookies are a browser storage mechanism. Sessions are a way to maintain server-side state.

5. CORS: Why Is the Browser Blocking My API? ๐Ÿšง

Imagine:

  • Your frontend runs at http://localhost:3000
  • Your backend runs at http://localhost:5000
  • You try to call http://localhost:5000/api/users

The browser may block your frontend JavaScript from accessing the response.

Why? Because these are different origins.

An origin consists of:

  • Scheme: http or https
  • Host: localhost
  • Port: 3000 or 5000

Even though both applications run on localhost, their ports are different, so their origins are different.

URLSame origin as http://localhost:3000?
http://localhost:3000/dashboardYes, only the path differs
http://localhost:5000/api/usersNo, the port differs
https://localhost:3000No, the scheme differs
http://127.0.0.1:3000No, the host differs

This is where CORS comes in. CORS stands for Cross-Origin Resource Sharing.

It is a browser-enforced mechanism that allows servers to specify which origins can access their responses.

This is the error you'll typically see in the browser console:

text
Access to fetch at 'http://localhost:5000/api/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

In short: CORS controls which cross-origin responses browser JavaScript is allowed to access.

6. How CORS Works โœˆ๏ธ

Your frontend sends a request to another origin. The backend can respond with a header such as:

http
Access-Control-Allow-Origin: http://localhost:3000

This tells the browser that the specified origin is allowed to access the response.

If the origin is not allowed, the browser prevents JavaScript from accessing the response.

One important detail: preflight requests

For certain cross-origin requests, the browser first sends an OPTIONS request. This is called a preflight request. It asks the server whether the actual request is permitted.

http
OPTIONS /api/users Origin: http://localhost:3000 Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type, Authorization

The server responds with the appropriate CORS headers:

http
HTTP/1.1 204 No Content Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: GET, POST Access-Control-Allow-Headers: Content-Type, Authorization

If the preflight succeeds, the browser proceeds with the actual request.

Not every cross-origin request requires a preflight:

RequestPreflight?
Simple GET with no custom headersUsually no
POST with Content-Type: application/jsonYes
Any request with an Authorization headerYes
PUT, PATCH or DELETEYes

In short: CORS uses HTTP headers, and sometimes an OPTIONS preflight, to control cross-origin browser access.

7. CORS Is Not Authentication ๐Ÿ›‘

Suppose your backend allows:

http
Access-Control-Allow-Origin: *

Does this mean everyone is authorized to access your API? No.

CORS is not an authentication or authorization mechanism. It primarily controls whether browser JavaScript from another origin can read a response.

A request made through Postman, curl or another backend server isn't restricted by browser CORS enforcement:

bash
# No browser, no CORS check. This works whatever your CORS settings are. curl -i http://localhost:5000/api/users

Your backend must still authenticate users, authorize operations and protect sensitive resources.

In short: CORS controls browser access to responses. It does not secure your API by itself.

8. Cookies and CORS Together ๐Ÿค

Now imagine your frontend and backend run on different origins, and you want to use session cookies.

Three things matter:

  1. The browser must be configured to include credentials in the cross-origin request.
  2. The backend must allow the specific frontend origin and credentials.
  3. The cookie's SameSite, Secure and other attributes must permit it to be sent.

For example, with fetch:

js
fetch("https://api.example.com/profile", { credentials: "include", });

And the backend needs appropriate CORS response headers, including:

http
Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Credentials: true

In Express, the cors package sets these headers and answers preflights for you:

js
import cors from "cors"; app.use(cors({ origin: "https://app.example.com", // a specific origin, never "*" credentials: true, }));

Key insight: When credentials are allowed, the server cannot use * as the allowed origin.

One more nuance: app.example.com and api.example.com are different origins but the same site. If your frontend and API are on completely different sites (say myapp.com and myapi.net), the cookie usually needs SameSite=None; Secure, and some browsers restrict these third-party cookies further.

In short: Cross-origin cookie-based sessions require coordination between browser settings, cookie attributes and backend CORS configuration.

Common mistakes

  • Using Access-Control-Allow-Origin: * together with credentials. The browser rejects it.
  • Forgetting credentials: "include" in fetch, so cross-origin cookies are never sent or stored.
  • Trying to "fix" CORS in frontend code. CORS is configured on the server.
  • Leaving HttpOnly off session cookies, so any injected script can read them.
  • Using an in-memory session store in production with multiple servers.

๐Ÿงช Try it yourself

  1. Open DevTools on a site you're logged in to. In Chrome, go to Application โ†’ Cookies (in Firefox, Storage โ†’ Cookies). Check the HttpOnly, Secure and SameSite columns.

  2. In the Console of the same site, type document.cookie. Cookies marked HttpOnly won't appear in the result.

  3. Run a local API and pretend to be a browser sending a preflight:

    bash
    curl -i -X OPTIONS http://localhost:5000/api/users \ -H "Origin: http://localhost:3000" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Content-Type"

    Look for the Access-Control-Allow-* headers. Then try an Origin your server doesn't allow.

๐ŸŽฏ Day 10 in one line

Cookies store data in the browser, sessions maintain state on the server, and CORS controls which cross-origin responses browser JavaScript can access.

Stateless HTTPCookiesSessionsCookie AttributesCORSPreflight Requests
Next chapterDay 11: Building an HTTP Server from Scratch, Then Express

Chapter 11 ยท Oct 31, 2026

Day 11: Building an HTTP Server from Scratch, Then Express

backend-seriesnodejsexpress-js

Key takeaway

Node's built-in http module creates the server and hands you req and res for every request. Express is a framework built on top of it that adds routing and response helpers, and app.listen() still starts a Node HTTP server underneath.

Ever wondered what actually happens when you write app.get("/users", ...) in Express?

What if I told you that Node.js can create an HTTP server without Express?

Let's build the understanding from scratch.

1. What Is an HTTP Server? ๐Ÿ–ฅ๏ธ

An HTTP server is a program that listens for incoming HTTP requests and sends back HTTP responses.

In Node.js, you don't need Express to create one. Node provides a built-in module called http. It gives you the basic tools to:

  • Create an HTTP server
  • Receive incoming requests
  • Read request information
  • Send HTTP responses
  • Listen on a specific port

Because http is built into Node.js, there is nothing to install.

In short: Express is not required to create an HTTP server. Node.js already provides the underlying functionality.

2. Your First Node.js Server ๐Ÿš€

Let's create a basic HTTP server. Save this as server.js:

js
const http = require("http"); const server = http.createServer((req, res) => { res.end("Hello from Node.js!"); }); server.listen(3000, () => { console.log("Server running on port 3000"); });

Run it with node server.js, then visit http://localhost:3000. You'll see:

text
Hello from Node.js!

What happened?

PieceWhat it does
http.createServer()Creates an HTTP server
The callback (req, res) => { ... }Runs every time a request reaches the server
reqRepresents the incoming request
resRepresents the response being constructed
server.listen(3000)Starts listening on port 3000

Key insight: You never call the callback yourself. Node calls it for you, once per request, and hands you fresh req and res objects each time.

In short: Node creates the server, receives requests, and gives you the objects needed to construct responses.

3. Understanding req and res ๐Ÿ“จ

These two objects are fundamental to backend development.

req (IncomingMessage)

Contains information about the incoming request.

PropertyWhat it holdsExample
req.methodHTTP methodGET
req.urlRequested URL (path and query string)/users?page=2
req.headersRequest headers{ host: "localhost:3000", ... }
js
const server = http.createServer((req, res) => { console.log(req.method); console.log(req.url); console.log(req.headers); // Always finish the response, or the browser keeps waiting res.end("Check your terminal"); });

If you visit GET /users, you can inspect the method, URL and headers received by your server in the terminal.

res (ServerResponse)

Used to construct and send the HTTP response.

Method or propertyWhat it does
res.statusCodeSet the status code
res.setHeader()Set response headers
res.write()Write response data (can be called many times)
res.end()Finish the response
js
const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader( "Content-Type", "application/json" ); res.end(JSON.stringify({ message: "Success" })); });

Notice that with the raw http module, you have to set the Content-Type header and turn the object into a JSON string yourself.

In short: req gives you the incoming request. res lets you construct and send the response.

4. Routing Without Express ๐Ÿ›ฃ๏ธ

Suppose you want two endpoints:

  • GET /users
  • GET /products

Using Node's HTTP module, you have to check the method and URL yourself.

js
const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/users") { res.end("Users list"); } else if (req.method === "GET" && req.url === "/products") { res.end("Products list"); } else { res.statusCode = 404; res.end("Route not found"); } });

It works! But imagine handling 50 endpoints, query parameters, request bodies, and different response formats.

You would have to handle yourselfWhy it gets painful
Matching method and URLA huge if/else chain
Query stringsreq.url is /users?page=2, so === "/users" fails
Route parametersExtracting 42 from /users/42 by hand
Request bodiesThe body arrives as a stream you must collect
JSON responsesHeaders and JSON.stringify every time

Your server code starts becoming difficult to manage.

In short: Node's HTTP module gives you control, but you have to handle much of the request processing yourself.

5. So, What Is Express? โšก

Express is a web framework built on top of Node.js HTTP functionality. It provides a higher-level interface for building web servers and APIs.

Instead of manually checking every method and URL, you can write (after npm install express):

js
const express = require("express"); const app = express(); app.get("/users", (req, res) => { res.send("Users list"); }); app.get("/products", (req, res) => { res.send("Products list"); }); app.listen(3000);

Notice the difference?

  • app.get() handles GET requests for a particular path.
  • req and res are still available.
  • app.listen() starts the server.
  • Unknown paths get a 404 reply (Cannot GET /orders) without any extra code.

Express handles much of the repetitive HTTP server work for you.

In short: Express provides a convenient abstraction over Node.js HTTP functionality.

6. Node HTTP vs Express โš–๏ธ

Node.js http moduleExpress
Where it comes fromBuilt into Node.jsExternal framework
LevelLow-level control over HTTP handlingConvenient, higher-level APIs
RoutingManual if/else on method and URLapp.get(), app.post() and friends
ResponsesStatus, headers and JSON.stringify by handres.status(), res.send(), res.json()
Amount of codeLots of repetitionMuch less repetitive code
Best forUnderstanding how servers workBuilding real APIs quickly

Think of it this way: Node.js HTTP is the foundation. Express is a framework that makes building on that foundation easier.

7. Does Express Replace Node HTTP? ๐Ÿ”„

No! Express still uses Node.js HTTP functionality underneath.

When you call app.listen(3000), Express ultimately creates and starts an HTTP server using Node.js. These two lines do the same job:

js
app.listen(3000); // is roughly a shortcut for: const http = require("http"); http.createServer(app).listen(3000);

You are not replacing Node's HTTP server. You are using a framework built around it.

Key insight: The req and res you get in Express are Node's own request and response objects, with extra helpers like res.json() and res.status() added on top.

8. What Breaks Without This? ๐Ÿ’ฅ

Imagine you start learning Express directly. You write:

js
app.get("/users", (req, res) => { res.json({ users: [] }); });

But you don't understand the questions below. Understanding Node's HTTP module answers them:

QuestionAnswer
Where does req come from?Node builds it from the incoming HTTP request
Who creates res?Node, a fresh one for every request
How does the server know which callback to run?Express matches the method and path to a route
What happens when a request reaches port 3000?Node's HTTP server, listening on that port, receives it
How does the response reach the browser?res writes it back over the same connection

You don't need to build every production application using the low-level HTTP module. But understanding the foundation makes debugging and learning frameworks much easier.

Common mistakes

  • Forgetting res.end() in a raw Node server, so the browser keeps loading forever.
  • Passing an object straight to res.end(). It only accepts strings or Buffers, so use JSON.stringify().
  • Sending JSON without a Content-Type: application/json header.
  • Starting a second server on a port that is already in use, which fails with EADDRINUSE.

๐Ÿงช Try it yourself

  1. Save the routing example from section 4 (with const http = require("http"); at the top and server.listen(3000); at the bottom) and run node server.js.
  2. In a second terminal, run curl -i http://localhost:3000/users, then curl -i http://localhost:3000/orders. Compare the 200 OK and 404 Not Found status lines.
  3. Now try curl -i "http://localhost:3000/users?page=2". You get a 404, because req.url includes the query string. This is exactly the kind of work Express does for you.

๐ŸŽฏ Day 11 in one line

Node.js provides the HTTP server functionality. Express builds a convenient abstraction on top of it, making backend development easier.

Phase 2 complete! ๐ŸŽฏ We've covered HTTP methods, URLs, parameters, headers, request bodies, status codes, HTTP versions, TLS, cookies, sessions and CORS.

Node HTTP ModuleRequest And Response ObjectsManual RoutingExpress FrameworkAbstraction Over Node HTTP
Next chapterDay 12: Setting Up Node.js and Express (Your First Backend Server)

Chapter 12 ยท Nov 1, 2026

Day 12: Setting Up Node.js and Express (Your First Backend Server)

backend-seriesnodejsexpress-js

Key takeaway

Node.js runs your code, npm manages dependencies through package.json, and Express builds the server. app.listen(3000) makes it reachable at http://localhost:3000, and npm scripts plus Nodemon make running it easy.

We've spent 11 days understanding how the backend works. Now it's time to build one.

Let's create our first Node.js + Express server from scratch.

1. What Do We Need? ๐Ÿงฐ

Before writing code, let's understand our tools.

ToolWhat it isIts job
Node.jsA JavaScript runtimeExecutes JavaScript outside the browser
npmA package manager (installed with Node.js)Installs and manages project dependencies
ExpressA web frameworkSimplifies building HTTP servers and APIs

Think of it this way: Node.js runs your backend code. npm manages the packages. Express helps you build the server.

Check that Node.js and npm are installed:

bash
node -v npm -v

If both print a version number, you're ready.

2. Create Your Project ๐Ÿ“

First, create a project directory:

bash
mkdir my-backend cd my-backend

Initialize a Node.js project:

bash
npm init -y

This creates a package.json file. The -y flag accepts all the default answers.

What is package.json?

It's the configuration file that describes your Node.js project. It contains:

  • Project name and version
  • Dependencies
  • Development scripts
  • Other project metadata

It will look roughly like this (the exact fields vary by npm version):

json
{ "name": "my-backend", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "license": "ISC" }

In short: npm init initializes your project and creates its package configuration.

3. Install Express ๐Ÿ“ฆ

Now install Express:

bash
npm install express

What happens here?

What you getWhat it is for
Express downloadednpm fetches Express and the packages it depends on
"dependencies" in package.jsonRecords that your project needs Express, e.g. "express": "^5.1.0" (your version may differ)
node_modules/ folderHolds the actual downloaded package code
package-lock.jsonRecords the exact dependency versions installed

You don't need to manually download or manage Express files.

Key insight: node_modules/ can always be rebuilt by running npm install, because package.json and package-lock.json describe exactly what belongs in it. That's why it is usually left out of Git.

In short: npm installs Express and manages the packages your application depends on.

4. Create Your First Express Server โœ๏ธ

Create a file named server.js:

js
const express = require("express"); const app = express(); app.get("/", (req, res) => { res.send("Backend server is running!"); }); app.listen(3000, () => { console.log("Server running on port 3000"); });

Let's understand what's happening.

LineWhat it does
require("express")Imports Express
express()Creates an Express application
app.get("/", ...)Defines a basic GET endpoint for the path /
reqRepresents the incoming request
resUsed to send the response
app.listen(3000, ...)Starts the server on port 3000 and runs the callback once it's ready

In short: You now have a working Express application listening for incoming HTTP requests.

5. Start Your Server โ–ถ๏ธ

Run:

bash
node server.js

You should see:

text
Server running on port 3000

Now open your browser at http://localhost:3000. You'll see:

text
Backend server is running!

Congratulations! Your first Express server is running locally. ๐ŸŽ‰

The terminal stays busy because the server keeps listening for requests. Press Ctrl + C to stop it.

6. What Is localhost? ๐Ÿ 

When you visit http://localhost:3000, you're accessing a server running on your own machine.

PartMeaning
http://The protocol your browser uses to talk to the server
localhostYour own machine (it points to the loopback address 127.0.0.1)
3000The port your server is listening on

Your browser sends an HTTP request to that port, Node.js receives it, Express handles the matching endpoint, and the response goes back to your browser.

In short: Localhost lets you test your backend on your own machine without deploying it to the internet.

7. Add npm Scripts โš™๏ธ

Instead of typing node server.js every time, we can add a script to package.json:

json
{ "scripts": { "start": "node server.js" } }

Now run:

bash
npm start

With plain node, you must stop and restart the server after every code change. You can install Nodemon for development:

bash
npm install -D nodemon

The -D flag saves it under devDependencies, because it's only needed while developing. Add another script:

json
{ "scripts": { "start": "node server.js", "dev": "nodemon server.js" } }

Now:

bash
npm run dev

Nodemon automatically restarts your server when it detects file changes.

CommandWhat it runsWhen to use it
npm startnode server.jsRunning the app normally
npm run devnodemon server.jsWhile you're writing code

start is a special script name, so npm start works without run. Custom names like dev need npm run dev.

In short: npm scripts simplify running your application, while Nodemon improves the development workflow.

8. Your Project Structure ๐Ÿ—‚๏ธ

At this stage, your project should look like this:

text
my-backend/ โ”‚ โ”œโ”€โ”€ node_modules/ โ”œโ”€โ”€ package.json โ”œโ”€โ”€ package-lock.json โ””โ”€โ”€ server.js

That's all you need to get your first Express server running. We'll introduce routes, controllers, services and a proper project structure on Day 13.

9. What Breaks Without This? ๐Ÿ’ฅ

Imagine you install Express, copy some code from a tutorial, and run it without understanding the setup. Your server doesn't start.

You don't know:

  • Which file starts the application.
  • Where dependencies are installed.
  • What app.listen() actually does.
  • Why your browser cannot connect to port 3000.

Understanding the setup makes debugging much easier.

What you seeLikely causeFix
Error: Cannot find module 'express'Express isn't installed in this folderRun npm install express (or npm install)
Cannot find module '.../server.js'Wrong folder or wrong filenamecd my-backend and check the name
EADDRINUSE: address already in use :::3000Another server is already on port 3000Stop it with Ctrl + C, or use another port
Browser says it can't connectThe server isn't running or crashedCheck the terminal and start it again
Code changes don't show upPlain node is still running old codeRestart, or use npm run dev

Common mistakes

  • Running npm commands outside the project folder, so package.json isn't found.
  • Committing node_modules/ to Git instead of adding it to .gitignore.
  • Forgetting to save the file before testing.
  • Listening on one port and visiting another in the browser.

You don't just want code that runs. You want to understand why it runs.

๐Ÿงช Try it yourself

  1. Start the server with npm run dev, then in another terminal run curl -i http://localhost:3000. Look for HTTP/1.1 200 OK, the X-Powered-By: Express header and your message.
  2. Change the text in res.send() and save. Watch Nodemon restart, then run the curl command again.
  3. Visit http://localhost:3000/hello. Express replies with a 404 and Cannot GET /hello, because no route matches that path yet.

๐ŸŽฏ Day 12 in one line

Node.js executes your backend code, npm manages dependencies, and Express provides the framework to build your HTTP server.

Node.js Runtimenpm And package.jsonInstalling ExpressLocalhost And Portsnpm ScriptsNodemon
Next chapterDay 13: Routing and Project Structure (Routes, Controllers, Services)

Chapter 13 ยท Nov 2, 2026

Day 13: Routing and Project Structure (Routes, Controllers, Services)

backend-seriesexpress-jsarchitecture

Key takeaway

Routes decide where a request goes, controllers handle req and res, and services hold reusable business logic that knows nothing about HTTP. The flow is Request, Route, Controller, Service, Controller, Response.

Imagine building an e-commerce backend with 50 API endpoints. Would you put all 50 endpoints inside server.js?

It might work initially, but maintaining it would quickly become difficult. Let's understand how backend developers organize their applications.

1. The One-File Problem ๐Ÿ“„

Imagine your server.js contains:

  • User registration and login
  • Product listing and searching
  • Cart management
  • Order creation
  • Payment processing

Every endpoint, database operation and business rule lives in the same file. As the application grows:

  • Finding code becomes difficult.
  • Changing one feature becomes risky.
  • Reusing business logic becomes harder.
  • Testing individual parts becomes inconvenient.

We need a better structure.

In short: A small application can start with one file, but growing applications need separation of responsibilities.

2. What Is Routing? ๐Ÿ›ฃ๏ธ

A route determines which code should handle an incoming HTTP request. A route is generally identified by:

  • HTTP method
  • URL path
js
// getUsers, createUser and friends are handler functions defined elsewhere app.get("/users", getUsers); app.post("/users", createUser); app.get("/products", getProducts); app.delete("/products/:id", deleteProduct);
MethodPathWhat it does
GET/usersRetrieves users
POST/usersCreates a user
GET/productsRetrieves products
DELETE/products/:idDeletes a product

The :id part is a route parameter. For DELETE /products/42, Express gives you req.params.id === "42".

Express matches the incoming request to the appropriate route handler.

In short: Routing connects incoming HTTP requests to the code responsible for handling them.

3. Express Router ๐Ÿงญ

Express provides express.Router() to create separate route modules. Instead of defining every endpoint inside server.js, we can organize routes by feature.

js
// routes/user.routes.js const express = require("express"); const router = express.Router(); router.get("/", (req, res) => { res.send("Get all users"); }); router.post("/", (req, res) => { res.send("Create user"); }); module.exports = router;

Now connect it to the main application:

js
// server.js const express = require("express"); const userRoutes = require("./routes/user.routes"); const app = express(); app.use("/users", userRoutes); app.listen(3000);

Now:

  • GET /users reaches the GET route.
  • POST /users reaches the POST route.

Notice that we don't need to repeat /users inside every route. The mount path and the router path are joined together:

Mount path in app.use()Path in the routerFull URL
/users//users
/users/:id/users/42

In short: Express Router lets us group related endpoints into separate modules.

4. Controllers ๐ŸŽฎ

A controller handles the incoming request and prepares the response. Instead of writing the request-handling logic directly inside the route file, we move it into a controller.

js
// controllers/user.controller.js const getUsers = (req, res) => { res.json({ users: ["Dev", "Sahil"] }); }; const createUser = (req, res) => { res.status(201).json({ message: "User created" }); }; module.exports = { getUsers, createUser };

Now our route file becomes:

js
// routes/user.routes.js const express = require("express"); const router = express.Router(); const { getUsers, createUser } = require("../controllers/user.controller"); router.get("/", getUsers); router.post("/", createUser); module.exports = router;

The route decides which controller function should execute. The controller handles the request and sends the response.

In short: Routes define where requests go. Controllers handle the incoming requests.

5. Services ๐Ÿง 

Now imagine creating a user requires:

  • Checking whether the email already exists.
  • Processing the user's information.
  • Creating the user record.
  • Returning the result.

Should all this business logic live inside the controller? Not necessarily. We can move the core business operations into a service.

js
// services/user.service.js const createUser = async (userData) => { // Business logic goes here return { id: "user123", name: userData.name, email: userData.email }; }; module.exports = { createUser };

The controller calls the service. Here is the full controller, now using it:

js
// controllers/user.controller.js const userService = require("../services/user.service"); const getUsers = (req, res) => { res.json({ users: ["Dev", "Sahil"] }); }; const createUser = async (req, res) => { const user = await userService.createUser(req.body); res.status(201).json(user); }; module.exports = { getUsers, createUser };

The service focuses on the application's business operations rather than handling HTTP requests and responses.

LayerKnows about req and res?Its job
RouteYesMaps a method and path to a controller
ControllerYesReads the request, calls the service, sends the response
ServiceNoRuns the business logic and returns data

Key insight: Because a service never touches req or res, the same function can be reused by several controllers, a background job or a test.

In short: Services contain reusable business logic. Controllers coordinate the request and response.

6. The Complete Request Flow ๐Ÿ”„

Let's say a client sends POST /users. Here's what happens:

  1. Server: Receives the incoming HTTP request.
  2. Router: Matches POST /users.
  3. Controller: Handles the request and extracts the required information.
  4. Service: Executes the business operation.
  5. Controller: Receives the result and prepares the response.
  6. Client: Receives the HTTP response.

Think of it as:

text
Request โ†’ Route โ†’ Controller โ†’ Service โ†’ Controller โ†’ Response

This is a common architecture, not a mandatory rule for every Express application.

7. Project Structure ๐Ÿ—‚๏ธ

A simple example:

text
my-backend/ โ”‚ โ”œโ”€โ”€ controllers/ โ”‚ โ””โ”€โ”€ user.controller.js โ”‚ โ”œโ”€โ”€ routes/ โ”‚ โ””โ”€โ”€ user.routes.js โ”‚ โ”œโ”€โ”€ services/ โ”‚ โ””โ”€โ”€ user.service.js โ”‚ โ”œโ”€โ”€ server.js โ”œโ”€โ”€ package.json โ””โ”€โ”€ package-lock.json

Each folder has a responsibility:

File or folderResponsibility
routes/Defines API endpoints
controllers/Handles requests and responses
services/Contains business logic
server.jsCreates and starts the application

Since createUser reads req.body, server.js needs one extra line so Express can read JSON bodies (more on this on Day 15):

js
// server.js const express = require("express"); const userRoutes = require("./routes/user.routes"); const app = express(); app.use(express.json()); // fills req.body from JSON requests app.use("/users", userRoutes); app.listen(3000, () => { console.log("Server running on port 3000"); });

As your application grows, you can organize additional features using the same pattern, for example product.routes.js, product.controller.js and product.service.js.

8. Folders Are Not Architecture โš ๏ธ

Creating folders doesn't automatically mean you have good architecture.

Common mistakes

  • Putting database queries and business rules everywhere.
  • Writing business logic directly inside route handlers.
  • Making controllers unnecessarily large.
  • Creating too many layers for a very small application.

The goal isn't to create more files. The goal is to keep responsibilities clear and make the code easier to maintain.

9. What Breaks Without Separation? ๐Ÿ’ฅ

Imagine your payment calculation logic is written directly inside five different controllers. Now the discount calculation changes.

You have to update five places. What if you miss one? Different parts of your application may calculate different prices.

If the calculation lives in a reusable service, multiple controllers can use the same business logic:

Change the discount rule once in the service, and every controller gets the update.

๐Ÿงช Try it yourself

  1. Create the route file from section 4, the service and full controller from section 5, and the server.js from section 7. Then run node server.js.
  2. Run curl -i http://localhost:3000/users. You should get 200 OK and {"users":["Dev","Sahil"]}.
  3. Create a user:
bash
curl -i -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name":"Dev","email":"dev@example.com"}'

You should get 201 Created and the user object returned by the service.

๐ŸŽฏ Day 13 in one line

Routes decide where requests go, controllers handle HTTP requests and responses, and services contain business logic.

RoutingExpress RouterControllersServicesRequest FlowProject Structure
Next chapterDay 14: Express Middleware (The Chain, next() and Writing Your Own)

Chapter 14 ยท Nov 3, 2026

Day 14: Express Middleware (The Chain, next() and Writing Your Own)

backend-seriesexpress-jsmiddleware

Key takeaway

Express runs middleware in the order you register it, and every middleware must either call next() to pass control forward or send a response itself. If it does neither, the request hangs.

Ever wondered how logging, request processing and other operations happen before your API controller executes?

That's where middleware comes in. Let's understand how Express processes a request internally.

1. What Is Middleware? ๐Ÿงฉ

Middleware is a function that executes during the request-response cycle. It gets access to three things:

ParameterWhat it is
reqThe incoming request
resThe response object
nextA function used to pass control to the next middleware

A middleware can:

  • Execute some code.
  • Read or modify the request.
  • Modify the response.
  • End the request-response cycle.
  • Pass control to the next middleware.

Think of airport security checkpoints. Each checkpoint inspects you and then either waves you on to the next one or stops you right there.

js
const myMiddleware = (req, res, next) => { // 1. Do some work // 2. Then call next() OR send a response next(); };

In short: Middleware sits between receiving a request and sending the final response.

2. Why Do We Need Middleware? ๐Ÿค”

Imagine an e-commerce backend with 50 API endpoints. Before processing requests, you might want to:

  • Log incoming requests.
  • Check request information.
  • Measure processing time.
  • Apply common processing logic.

Would you write the same code inside all 50 endpoints? That creates duplication and makes maintenance difficult.

Instead, middleware lets you write common functionality once and reuse it across multiple routes.

In short: Middleware helps separate reusable request-processing logic from individual route handlers.

3. How the Middleware Chain Works โ›“๏ธ

Imagine a request arriving at your Express server. It passes through a sequence of middleware functions before reaching the route handler.

js
const express = require("express"); const app = express(); app.use((req, res, next) => { console.log("Middleware 1"); next(); }); app.use((req, res, next) => { console.log("Middleware 2"); next(); }); app.get("/", (req, res) => { res.send("Hello from backend!"); }); app.listen(3000);

When you visit /, the output is:

text
Middleware 1 Middleware 2

Then the route handler sends the response. The execution flow:

text
Request โ†’ Middleware 1 โ†’ Middleware 2 โ†’ Route Handler โ†’ Response

Express processes middleware in the order in which it is registered.

In short: Express executes matching middleware sequentially. Order matters.

4. What Exactly Does next() Do? โžก๏ธ

This is where beginners often get confused.

next() tells Express to pass control to the next matching middleware or handler in the processing chain.

js
app.use((req, res, next) => { console.log("First"); next(); }); app.use((req, res, next) => { console.log("Second"); next(); }); app.get("/", (req, res) => { console.log("Third"); res.send("Done"); });

Output:

text
First Second Third

But what happens if you remove next()?

js
app.use((req, res, next) => { console.log("First"); });

The request gets stuck because this middleware neither passes control forward nor sends a response.

Important: A middleware must either pass control forward or complete the response. Otherwise, the request can hang.

A middleware is allowed to end the chain itself. For example:

js
const MAINTENANCE_MODE = true; app.use((req, res, next) => { if (MAINTENANCE_MODE) { return res.status(503).send("Down for maintenance"); } next(); });

The return matters here: it stops the function so next() is not also called after the response is sent.

In short: next() moves processing forward. It does not itself send a response.

5. Middleware Can Modify the Request โœ๏ธ

Middleware doesn't just execute code. It can also attach information to the request object.

js
app.use((req, res, next) => { req.requestTime = new Date(); next(); }); app.get("/", (req, res) => { res.json({ time: req.requestTime }); });

Here:

  1. Middleware receives the request.
  2. It adds requestTime to req.
  3. It calls next().
  4. The route handler accesses the added property.

This is useful when information needs to be shared between middleware and route handlers.

In short: Middleware can enrich the request before it reaches the route handler.

6. Application-Level vs Route-Level ๐ŸŽฏ

Middleware doesn't always need to run for every endpoint.

Application-level middleware

Registered using app.use():

js
app.use((req, res, next) => { console.log("Request received"); next(); });

This runs for matching requests across the application.

Route-level middleware

Applied only to specific routes:

js
const checkRequest = (req, res, next) => { console.log("Checking this route"); next(); }; app.get("/profile", checkRequest, (req, res) => { res.send("Profile"); }); app.get("/products", (req, res) => { res.send("Products"); });

Here, checkRequest runs for /profile, but not /products.

Application-levelRoute-level
Registered withapp.use(fn)app.get(path, fn, handler)
Runs forMatching requests across the appOnly the routes you attach it to
Typical useLogging, reading request bodiesChecks needed by one or a few routes

In short: Application-level middleware applies broadly. Route-level middleware applies to selected routes.

7. Writing Your Own: A Request Logger ๐Ÿ› ๏ธ

Let's create a simple request logger.

js
const requestLogger = (req, res, next) => { const start = Date.now(); console.log( `${req.method} ${req.url}` ); res.on("finish", () => { console.log( `Completed in ${Date.now() - start}ms` ); }); next(); }; app.use(requestLogger);

Now every matching request logs:

text
GET /users Completed in 12ms POST /products Completed in 8ms

Notice something interesting: the middleware starts before the route handler, but the finish event runs when the response has been sent. This lets us measure the complete response time.

In short: You can create custom middleware to implement reusable functionality specific to your application.

8. What Breaks Without This? ๐Ÿ’ฅ

Imagine you write:

js
app.use((req, res, next) => { console.log("Request received"); }); app.get("/users", (req, res) => { res.send("Users"); });

You open /users, but your browser keeps loading. Why?

Because the middleware never calls next() and never sends a response. The route handler is never reached.

This is one of the most common middleware mistakes.

Common mistakes

  • Forgetting to call next(), so the request hangs.
  • Sending a response and then also calling next(). A later handler tries to respond again and you get Cannot set headers after they are sent to the client.
  • Registering middleware after a route. If that route already sent the response, the middleware never runs for it.
  • Writing checkRequest() instead of checkRequest in a route. The parentheses call the function immediately instead of handing it to Express.

9. The Complete Picture ๐Ÿ–ผ๏ธ

PieceResponsibility
RequestCarries the incoming HTTP request
MiddlewareExecutes reusable processing logic
next()Passes control forward
Route handlerHandles the matched endpoint and typically sends the response

A typical flow looks like:

text
Client โ†’ Middleware 1 โ†’ Middleware 2 โ†’ Route Handler โ†’ Response

The chain can also end inside a middleware if it sends the response itself.

๐Ÿงช Try it yourself

  1. Run the server from section 3 and visit http://localhost:3000. Check your terminal for Middleware 1 and Middleware 2 (you may see them twice, because browsers also request /favicon.ico).
  2. Delete next(); from Middleware 1, restart, and run curl -i --max-time 5 http://localhost:3000/. Nothing comes back, and curl gives up after 5 seconds with a timeout error.
  3. Put next() back, then register the requestLogger from section 7 with app.use() before the other middleware, and hit a few URLs. Watch each request log its method, URL and timing.

๐ŸŽฏ Day 14 in one line

Middleware functions process requests in sequence. next() passes control forward, and custom middleware lets you reuse logic across your Express application.

MiddlewareMiddleware ChainThe next() FunctionApplication-Level MiddlewareRoute-Level MiddlewareCustom Middleware
Next chapterDay 15: Built-in and Third-Party Middleware (JSON, Morgan, CORS, Helmet, Rate Limiting)

Chapter 15 ยท Nov 4, 2026

Day 15: Built-in and Third-Party Middleware (JSON, Morgan, CORS, Helmet, Rate Limiting)

backend-seriesexpress-jsmiddleware

Key takeaway

Use express.json() and trusted packages like morgan, cors, helmet and express-rate-limit instead of reinventing them, and register them before your routes in a deliberate order. CORS is not authentication, and Helmet alone does not make an app secure.

In the last chapter, we learned how middleware works and how to write our own.

But here's the thing: you don't need to build everything from scratch. Express has built-in middleware, and the ecosystem provides packages for common backend tasks.

Let's understand the important ones. To follow along, install the third-party packages:

bash
npm install morgan cors helmet express-rate-limit

1. Built-in Middleware ๐Ÿ“ฆ

Express provides middleware for handling common request data.

js
app.use(express.json());

This parses incoming JSON request bodies and makes the data available through req.body.

Other useful built-in middleware:

MiddlewareWhat it handles
express.json()JSON request bodies, placed in req.body
express.urlencoded({ extended: true })URL-encoded form data (classic HTML forms), placed in req.body
express.static("public")Serves static files such as images, CSS and HTML from a folder

Here is the difference express.json() makes:

js
const express = require("express"); const app = express(); app.use(express.json()); app.post("/users", (req, res) => { res.status(201).json({ received: req.body }); }); app.listen(3000);

Key insight: Without express.json(), req.body is undefined, even if the client sent perfectly valid JSON.

2. Logging With Morgan ๐Ÿ“

How do you know which requests are reaching your server? Morgan logs incoming HTTP requests.

js
const morgan = require("morgan"); app.use(morgan("dev"));

It logs details such as the HTTP method, URL, status code, and response time:

text
GET /api/users 200 4.512 ms - 25 GET /api/orders 404 1.873 ms - 149

Useful for debugging and monitoring requests. It's the same idea as the request logger we wrote on Day 14, but ready-made and with several log formats (for example "dev" for development and "combined" for production-style logs).

3. CORS Middleware ๐ŸŒ

Your frontend runs on one origin and your backend on another. The browser may block frontend JavaScript from accessing the backend response unless the server allows that origin.

That's where CORS comes in.

js
const cors = require("cors"); app.use(cors({ origin: "http://localhost:5173" // your frontend's origin }));

Our backend runs on port 3000, so here we allow a frontend on a different port, such as a Vite dev server on 5173. Use your real frontend URL.

CORS controls which origins are allowed to access your resources through browsers.

Notice that the browser does the blocking. Tools like curl or Postman ignore CORS completely.

Important: CORS is not authentication or a replacement for backend authorization.

4. Helmet ๐Ÿช–

Your API should send appropriate HTTP security headers. Helmet helps by setting several security-related headers.

js
const helmet = require("helmet"); app.use(helmet());

A few of the headers it handles:

HeaderWhat it does
Content-Security-PolicyLimits where scripts, styles and images can load from
Strict-Transport-SecurityTells browsers to use HTTPS only
X-Content-Type-Options: nosniffStops browsers from guessing content types
X-Frame-Options: SAMEORIGINStops other sites from embedding your pages
X-Powered-ByRemoved, so you don't advertise that you use Express

It provides useful security defaults, but it doesn't automatically make your application secure. You still need validation, authentication and authorization.

5. Rate Limiting ๐Ÿšฆ

What happens if someone sends thousands of requests to your API? Your server can become overloaded.

Rate limiting restricts how many requests a client can make within a defined time window.

js
const rateLimit = require("express-rate-limit"); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 100 }); app.use(limiter);

This example allows up to 100 requests per 15 minutes per client, based on the limiter's configured keying strategy (by default, the client's IP address). Once a client goes over the limit, it gets 429 Too Many Requests until the window resets.

6. Putting Them Together ๐Ÿ”—

Here is a complete, runnable server.js with every piece in place:

js
const express = require("express"); const morgan = require("morgan"); const cors = require("cors"); const helmet = require("helmet"); const rateLimit = require("express-rate-limit"); const app = express(); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 100 }); // A stub router standing in for your real API routes const apiRoutes = express.Router(); apiRoutes.get("/users", (req, res) => { res.json({ users: ["Dev", "Sahil"] }); }); app.use(express.json()); app.use(morgan("dev")); app.use(cors({ origin: "http://localhost:5173" })); app.use(helmet()); app.use(limiter); app.use("/api", apiRoutes); app.listen(3000, () => { console.log("Server running on port 3000"); });

Middleware order matters. Express executes middleware in the order you register it.

MiddlewareWhat it doesWhy it is placed here
express.json()Parses JSON bodies into req.bodyFirst, so everything after it can read req.body
morgan("dev")Logs each requestEarly, so every request is logged, even ones rejected later
cors(...)Adds CORS headers and answers preflight requestsBefore the routes, so every API response carries CORS headers
helmet()Sets security headersBefore the routes, so every response gets them
limiterCounts requests per clientRight before the routes, so excess requests never reach them
app.use("/api", apiRoutes)Your actual endpointsLast, after the request has passed through the whole pipeline

This is not the only valid order. Some teams register helmet() first, or put the rate limiter before body parsing so blocked requests do less work. What matters is choosing the order on purpose.

Common mistakes

  • Forgetting express.json(), then wondering why req.body is undefined.
  • Registering middleware after the routes, so it never runs for them.
  • Thinking CORS protects your API from curl, Postman or other servers. It only guides browsers.
  • Treating helmet() as complete security.
  • Deploying behind a proxy without configuring app.set("trust proxy", ...), so the rate limiter sees the proxy's IP instead of each client's.

7. Remember ๐Ÿง 

IdeaIn one line
Built-in middlewareHandles common Express tasks like parsing bodies
Third-party middlewareSaves you from reinventing common functionality
MorganLogs every request that reaches your server
CORSControls browser cross-origin access
HelmetAdds security-related HTTP headers
Rate limitingHelps protect APIs from excessive requests
OrderExpress runs middleware in the order you register it

A good backend isn't just about writing routes. It's also about building the right request-processing pipeline.

๐Ÿงช Try it yourself

  1. Save the code from section 6 as server.js and run node server.js.
  2. Run curl -i http://localhost:3000/api/users. Spot the Helmet headers (Content-Security-Policy, X-Content-Type-Options), the Access-Control-Allow-Origin header and the missing X-Powered-By. Check your terminal for Morgan's log line.
  3. Change limit: 100 to limit: 3, restart, and run:
bash
for i in 1 2 3 4; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/users; done

You should see 200 three times, then 429.

๐ŸŽฏ Day 15 in one line

Let built-in middleware handle common request data, use trusted packages for logging, CORS, security headers and rate limiting, and register them in a deliberate order, because a good backend is a well-built request-processing pipeline.

Built-in MiddlewareMorgan LoggingCORSHelmet Security HeadersRate LimitingMiddleware Order

Written with DevJournal โ€” a learning journal that helps developers remember what they learn.

Start your own journal free