Express isn't just a framework that makes routing easier. Its real power comes from the Request/Response Lifecycle and Middleware architecture. By learning these two concepts you'll understand most of Express internals and be able to confidently debug real-world applications.
So far, we've seen that raw Node.js is powerful.
But after writing a few routes, things get messy quickly.
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/users") {
res.end("Users");
} else if (req.url === "/products") {
res.end("Products");
} else {
res.end("404");
}
});
Imagine maintaining this with:
- 100 routes
- Authentication
- Validation
- Logging
- Error handling
That's a nightmare.
Express solves this by introducing:
- Clean routing
- Middleware
- Request parsing
- Error handling
- Extensibility
In this article, we'll build a mental model of how Express works internally.
π― What You'll Learn
By the end of this guide you'll understand:
β Scaffolding
β Request lifecycle
β Request object
β Response object
β Middleware architecture
β Middleware execution order
β Error handling
π Prerequisites
You should be comfortable with:
- Basic JavaScript
- Node.js fundamentals
- npm
- HTTP basics
If you've completed the previous Node.js article, you're ready.
ποΈ What is Scaffolding?
When we hear the word scaffolding, it often sounds complicated.
In reality, it's a construction term.
When a building is being built, workers first create temporary support structures called scaffolds.
Only then do they start building floors, walls, and rooms.
The same idea applies to software development.
Scaffolding is the process of creating the initial structure of an application before writing actual business logic.
Think of it as preparing the foundation of your project.
Before building APIs, authentication, databases, and features, we first decide:
- Where routes will live
- Where middleware will live
- Where controllers will live
- Where configuration files will live
- How the project will be organized
Without scaffolding, projects quickly become difficult to maintain.
Imagine building a house.
You don't start by placing random bricks.
You first create a blueprint.
House
β
βββ Living Room
βββ Kitchen
βββ Bedroom
βββ Bathroom
Because each room has a specific purpose.
Similarly, in an Express application:
Application
β
βββ Routes
βββ Controllers
βββ Middleware
βββ Models
βββ Config
Each folder has a specific responsibility.
This organization makes the project easier to understand and maintain.
π Understanding the Request/Response Lifecycle
π« Think of It Like Airport Security
When you board a flight:
A web request works similarly:
π Lifecycle Overview
π₯ What Happens Internally?
Suppose the browser sends:
GET /users/42?page=1 HTTP/1.1
Express processes the request in stages.
Stage 1: Express Creates req and res
Express generates:
req
and
res
objects.
These travel together through the entire lifecycle.
Think of them as a package moving through a conveyor belt.
Stage 2: Middleware Executes
Every registered middleware runs.
app.use(logger);
app.use(auth);
Execution order:
logger
β
auth
β
route handler
Order matters.
Always.
Stage 3: Route Matching
Express searches for matching routes.
app.get("/users/:id");
Request:
/users/42
Match found β
Handler executes.
Stage 4: Business Logic Runs
Database queries.
Validation.
Authorization.
Processing.
Everything happens here.
Stage 5: Response Sent
res.json(...)
or
res.send(...)
Browser receives response.
Lifecycle ends.
Now we know the lifecycle of a request - but let's zoom out. Your API receives an HTTP Request with multiple compartments. Let's understand which compartment holds which data, and how Express extracts it.
What is an HTTP Request?
At its core, an HTTP request is just a plain text string sent over a TCP socket from a client (browser, mobile app, cURL) to a server.
That string follows a strict format defined by the HTTP protocol. It looks like this:
POST /api/users?role=admin HTTP/1.1
Host: mywebsite.com
Authorization: Bearer xyz123
Content-Type: application/json
Content-Length: 27
{ "name": "Rama", "age": 25 }
This text is broken down into 4 distinct parts by your web framework (Express, Fastify, etc.). They are:
- The Request Line (Method + Path + Query + Protocol)
- The Headers (Metadata)
- The Body (Data payload)
- The Route Parameters (Dynamic segments in the URL path)
Express takes this raw string, parses it into a JavaScript object (req), and gives it to you. Let's decode every piece.
HTTP Request Types (The Methods)
When we say "request types", we mean HTTP Methods (or Verbs). These define the intent of the request.
| Method | Intent | Is it Idempotent?* | Has a Body? |
|---|---|---|---|
| GET | Fetch data. | Yes | No (Never) |
| POST | Create a new resource. | No | Yes |
| PUT | Replace an entire resource entirely. | Yes | Yes |
| PATCH | Partially update a resource. | No | Yes |
| DELETE | Remove a resource. | Yes | No (Usually) |
| OPTIONS | Ask the server what methods are allowed (CORS preflight). | Yes | No |
| HEAD | Same as GET, but only returns headers, not the body. | Yes | No |
*Idempotent means making the exact same request 1 time or 100 times produces the exact same result on the server (no side effects). If you PUT the same data 10 times, the final state is identical.
Crucial Rule: You access these in Express via req.method.
app.use((req, res, next) => {
if (req.method === 'GET') console.log('Fetching data');
if (req.method === 'POST') console.log('Creating data');
next();
});
π₯ Understanding the Request Object (req)
The first line of an HTTP request is: METHOD PATH PROTOCOL.
Express splits that PATH into two specific parts: Route Parameters and Query Strings.
1. req.params (Dynamic Values in the URL)
These are named placeholders you define in your route path. They are part of the URL's structural hierarchy.
Suppose your application has the following route:
app.get("/users/:id", (req, res) => {
console.log(req.params);
});
Notice the :id?
That colon tells Express:
"This part of the URL is dynamic."
Now imagine someone visits:
/users/42
Express automatically extracts the value and stores it inside req.params.
Output:
{
id: "42"
}
When should you use req.params?
Whenever you're identifying a specific resource.
Examples:
/users/42/products/101/orders/9001
Think of it like a house address.
Without the address, the delivery person doesn't know which house to visit.
Similarly, without an ID, your server doesn't know which user, product, or order you're referring to.
2. req.query (Optional Information)
Now consider this URL:
/users?page=2&limit=10
Everything after the ? is called the query string.
Express automatically converts it into:
req.query
Output:
{
page: "2",
limit: "10"
}
Unlike route parameters, query parameters are optional.
The same route works perfectly fine without them.
Common use cases
- Pagination
/users?page=2
- Searching
/users?search=express
- Filtering
/products?category=laptops
- Sorting
/products?sort=price
π‘ Pro Tip: Every value inside
req.queryis a string. If you need a number, convert it usingNumber()orparseInt().
req.headers (Metadata About the Request)
Headers are key-value pairs sent before the body. They contain metadata about the request, not the actual data itself.
Common headers you will use daily:
| Header | Purpose |
|---|---|
Authorization |
Sends credentials (Bearer tokens, Basic Auth). |
Content-Type |
Tells the server how the body is formatted (application/json, multipart/form-data, text/plain). |
Accept |
Tells the server what format the client wants back (application/json, text/html). |
Cookie |
Sends stored session cookies from the browser. |
User-Agent |
Identifies the client's browser/OS. |
Express behavior: Express lowercases all header names.
// Incoming header: Authorization: Bearer abc123
console.log(req.headers.authorization); // "Bearer abc123"
console.log(req.headers['content-type']); // "application/json"
π‘ We'll dive much deeper into request headers when we build authentication and authorization systems later in this series.
req.body (The Actual Data)
When the client wants to create or update something, it usually sends data inside the request body.
For example:
{
"name": "Raman",
"role": "Developer"
}
Express makes this data available through:
console.log(req.body);
Output:
{
name: "Raman",
role: "Developer"
}
Simple enough...
Well... almost π
By default, Express doesn't know how to read JSON data.
You need to tell it to parse incoming JSON requests by registering the built-in JSON middleware.
app.use(express.json());
Without this middleware:
console.log(req.body);
Output:
undefined
This is probably one of the most common mistakes every beginner and experienced makes.
We'll understand why express.json() is required and how middleware works internally in the Middleware section.
When to use req.body:
- POST/PUT/PATCH requests where you are sending structured data (e.g.,
{ "email": "ramanand@zohomail.com", "password": "123" }). -
Critical Rule: A GET request cannot have a
req.body. If you put a body on a GET request, most servers and proxies will ignore or reject it.
The "Hidden" Request Properties
Express adds a few extra helpful properties to the req object that you will use:
| Property | What it gives you |
|---|---|
req.ip |
The IP address of the client. |
req.path |
The URL path without the query string. (e.g., /users/42/posts) |
req.hostname |
The domain name (e.g., mywebsite.com). |
req.protocol |
http or https. |
req.cookies |
Requires middleware (cookie-parser). Gives you parsed cookies. |
req.signedCookies |
Cookies that have been cryptographically signed to prevent tampering. |
So when a request hits your server, the data flows into exclusive zones. They do not overlap.
-
URL Path β
req.params(e.g.,/users/:id) -
URL Search β
req.query(e.g.,?filter=active) -
HTTP Headers β
req.headers(e.g.,Authorization) -
HTTP Body β
req.body(e.g.,{ "name": "John" })
π‘ Pro Tip: Never put a password or large data in
req.paramsorreq.querybecause URLs are logged by servers and browsers. Always send sensitive data (passwords, credit cards) in thereq.bodyover HTTPS.
Here is how a good developer routes their data:
app.delete('/api/products/:productId', (req, res) => {
// 1. Identify WHAT to delete (Path param)
const { productId } = req.params;
// 2. Identify WHO is deleting (Auth header)
const userId = req.headers['x-user-id']; // Custom header for internal auth
// 3. Check for optional admin override (Query param for dev tools)
const forceDelete = req.query.force === 'true';
// Logic...
});
π€ Understanding HTTP Response Status Codes
So far, we've seen how a client sends a request to the server.
But what happens after the server processes that request?
It needs to tell the client what happened.
Did everything go well?
Was something missing?
Did the client send invalid data?
Or did the server itself run into a problem?
That's exactly what HTTP status codes are for.
Think of them as the server's way of saying:
"Here's what happened with your request."
π§ Status Codes are Organized into Families
Instead of remembering dozens of individual status codes, it's much easier to remember their families.
|Status Code | Meaning|
|--|--|
| 2xx | β
Success β Everything worked |
| 3xx | π Redirection β Go somewhere else |
| 4xx | β Client Error β Something is wrong with the request |
| 5xx | π₯ Server Error β Something went wrong on the server |
For most Express applications, you'll spend the majority of your time working with 2xx, 4xx, and 5xx responses.
π Which Status Code Should You Return?
Here's a simple cheat sheet that you'll use in almost every REST API.
| Request | Success | Common Failure | Why? |
|---|---|---|---|
| GET | 200 OK |
404 Not Found |
Data was returned successfully. If the requested resource doesn't exist, return 404. |
| POST | 201 Created |
400 Bad Request |
A new resource was created. If the client sends invalid data, return 400. |
| PUT |
200 OK or 204 No Content
|
400 Bad Request |
The existing resource was updated or replaced. |
| PATCH | 200 OK |
400 Bad Request |
Part of an existing resource was updated. |
| DELETE |
204 No Content or 200 OK
|
404 Not Found |
The resource was deleted. If it doesn't exist, return 404. |
Don't worry if you don't remember all of these right away.
After building a few APIs, these status codes become second nature.
β 200 OK
This is the most common status code you'll ever return.
It simply means:
"Your request was successful."
For example:
app.get("/users", (req, res) => {
res.status(200).json([
{
id: 1,
name: "John"
}
]);
});
The server successfully found the users and returned them.
π 201 Created
A common beginner mistake is returning 200 OK after creating a new resource.
Technically, it works.
But it's not the best choice.
If your API creates something new, the more appropriate response is:
201 Created
Example:
app.post("/users", (req, res) => {
const user = {
id: 101,
...req.body
};
res.status(201).json(user);
});
The response tells the client:
"Your request didn't just succeedβwe actually created something."
π‘ Pro Tip
Professional APIs often include a Location header pointing to the newly created resource.
res
.status(201)
.location("/users/101")
.json(user);
This makes it easy for clients to immediately fetch the new resource if needed.
π« 400 Bad Request
This error means:
"I understood your request, but the data you sent is invalid."
Imagine trying to register with:
{
"email": "not-an-email",
"password": ""
}
The request reached your server.
Your route executed.
But the data failed validation.
Example:
app.post("/users", (req, res) => {
if (!req.body.email) {
return res.status(400).json({
message: "Email is required"
});
}
// Continue creating user...
});
Notice something important.
The server isn't broken.
The client needs to fix the request.
π 404 Not Found
This one is probably the easiest to understand.
It simply means:
"The resource you're looking for doesn't exist."
Example:
GET /users/999
If user 999 doesn't exist:
res.status(404).json({
message: "User not found"
});
Unlike 400, there is nothing wrong with the request format.
The problem is that the requested resource simply doesn't exist.
π₯ 500 Internal Server Error
This is the status code every developer hopes never reaches production.
It means:
"Something went wrong inside the server."
Maybe:
Database connection failed
Third-party API is down
Unexpected exception occurred
Bug in your code
Example:
app.get("/users", async (req, res) => {
try {
const users = await database.getUsers();
res.json(users);
} catch (error) {
res.status(500).json({
message: "Something went wrong."
});
}
});
Unlike 400, this isn't the client's fault.
The server failed while processing a perfectly valid request.
π€ Understanding the Response Object (res)
We've spent quite a bit of time looking at the request object (req)βeverything the client sends to our server.
But a conversation isn't complete if only one side talks.
After processing the request, the server needs to send something back.
That's where the Response Object (res) comes in.
app.get("/users", (req, res) => {
// Send a response back to the client
});
Think of it like a conversation.
The browser asks a question.
The server answers it.
Everything the server sends backβwhether it's plain text, JSON data, an HTML page, an image, or even an error messageβgoes through the res object.
Let's look at the methods you'll use most often.
π¬ res.send() β Send Almost Anything
The simplest way to respond to a client is with res.send().
app.get("/", (req, res) => {
res.send("Hello, Express!");
});
The browser receives:
Hello, Express!
Simple enough.
But here's the cool part...
res.send() is smart.
It can send:
Strings
HTML
Buffers
Objects
Arrays
For example:
res.send("<h1>Welcome!</h1>");
renders HTML in the browser.
While this:
res.send({
name: "John"
});
automatically sends JSON.
Even though res.send() can send almost anything, most modern APIs prefer using res.json() when returning JSON data because it makes your intention much clearer.
π¦ res.json() β Send JSON Responses
Most Express applications today are REST APIs.
And REST APIs almost always communicate using JSON.
Instead of writing:
res.send({
success: true
});
it's more common to write:
res.json({
success: true
});
The client receives:
{
"success": true
}
Behind the scenes, Express automatically does two important things for you:
Converts your JavaScript object into JSON.
Sets the correct response header.
Content-Type: application/json
You don't have to call JSON.stringify() or manually set the content type.
Express takes care of everything.
That's why you'll see res.json() in almost every Express API.
π¦ res.status() β Tell the Client What Happened
Sending data is only half the story.
The client also needs to know whether the request succeeded or failed.
That's exactly what res.status() is for.
For example:
res.status(404);
sets the HTTP status code to 404 Not Found.
In practice, you'll almost always chain it with another response method.
app.get("/users/:id", (req, res) => {
// Imagine the user doesn't exist...
res.status(404).json({
message: "User not found"
});
});
The client now receives two things:
Status Code
404 Not Found
Response Body
{
"message": "User not found"
}
This tells the client not only what happened, but also why it happened.
π Method Chaining
One thing you'll notice in Express is that response methods are often chained together.
For example:
res.status(201).json(user);
or
res.status(500).send("Internal Server Error");
This works because methods like res.status() return the response object itself, allowing you to immediately call another method.
It makes the code concise and very readable.
π§ Putting It All Together
Let's build a small example that combines everything we've learned so far.
app.post("/users", (req, res) => {
const user = {
id: 101,
...req.body
};
res
.status(201)
.json(user);
});
When the client sends:
{
"name": "Rama",
"role": "Developer"
}
The server responds with:
Status
201 Created
Body
{
"id": 101,
"name": "Rama",
"role": "Developer"
}
Notice how the response communicates both the result (a new user was created) and the data (the newly created user).
π₯ Middleware
Now we're at the heart of Express.
π₯ Middleware β The Superpower Behind Express
So far, we've learned how a request reaches our Express application and how we can send a response back.
But what if you want to:
Log every incoming request?
Check if a user is authenticated?
Parse JSON data?
Validate incoming requests?
Compress responses?
Handle errors?
Surely you don't want to write the same code inside every single route.
That's exactly the problem middleware solves.
In fact, middleware is one of the biggest reasons Express became so popular.
π€ What Exactly is Middleware?
At its core, middleware is simply a function that runs between the incoming request and the outgoing response.
Think of it like a security checkpoint at an airport.
Every passenger has to pass through security before boarding the plane.
Similarly, every request can pass through one or more middleware functions before reaching your route handler.
Instead of handling everything inside your routes, you can move common tasks into reusable middleware.
π¦ The Middleware Pipeline β How Every Request Travels Through Express
π§ Think of Middleware as a Conveyor Belt
Imagine a package moving through a factory.
Before it reaches the customer, it passes through several stations.
Each station performs one specific task and then passes the package to the next station.
Middleware works exactly the same way.
Each middleware performs one job and then hands the request to the next middleware in the chain.
That's the entire middleware pipeline.
π οΈ Anatomy of a Middleware Function
A middleware function always receives three parameters.
const logger = (req, res, next) => {
console.log(req.method);
next();
};
Let's understand each one.
| Parameter | Purpose |
|---|---|
req |
The incoming request from the client |
res |
The response that will eventually be sent back |
next |
A function that tells Express to continue to the next middleware or route |
You've already seenreq and res.
The new character here is next().
And it's arguably the most important function in all of Express.
π¦ Understanding next()
Think of next() as saying:
"I'm done with my work. Let the next person continue."
Let's look at a simple example.
const logger = (req, res, next) => {
console.log("π₯ Request received");
next();
};
Every time a request arrives:
The message is logged.
next()is called.Express moves on to the next middleware (or the route handler).
Simple.
But here's where many get stuck...
π± What Happens If You Forget next()?
Suppose you accidentally write this:
const logger = (req, res, next) => {
console.log("π₯ Request received");
// Oops...
// Forgot to call next()
};
Now imagine a request comes in.
The logger runs.
It prints the message.
And then...
Nothing.
Express has no idea what to do next.
It keeps waiting for the middleware to either:
send a response, or
pass control to the next middleware.
Since neither happened, the request gets stuck forever.
From the browser's perspective, it looks like this:
Loading...
Loading...
Loading...
Loading...
The page never finishes loading because Express is still waiting.
This is one of the most common "silent bugs" beginners encounter.
Your server doesn't crash.
It doesn't throw an error.
The request simply hangs.
Whenever this happens, the first thing to check is:
"Did I forget to call
next()or send a response?"
π§© Why Does This Happen?
Express processes middleware like people standing in a queue.
Each middleware has exactly two responsibilities:
Option 1: Pass the request forward
next();
This tells Express:
"I'm done. Continue processing the request."
Option 2: End the request
For example:
res.send("Access Denied");
or
res.json({
message: "Success"
});
or
res.status(401).json({
message: "Unauthorized"
});
Once a response is sent, the request lifecycle ends.
No other middleware or routes will run after that.
π§© Types of Middleware
Not every middleware behaves the same way.
Some run for every request.
Some only run on specific routes.
Some are built into Express.
Others come from third-party packages.
Let's look at each type.
π Application Middleware
Application middleware runs for every incoming request.
It's registered using app.use().
app.use((req, res, next) => {
console.log("π₯ Incoming request");
next();
});
Now every request is logged.
Whether someone visits:
/
or
/users
or
/products/101
this middleware runs first.
Application middleware is commonly used for:
Logging
Authentication
Request parsing
Security
Rate limiting
Think of it as the building's main entrance.
Everyone passes through it.
π― Route Middleware
Sometimes you don't want middleware to run for every request.
Maybe only authenticated users should access the dashboard.
Instead of protecting every route, you can protect just one.
app.get(
"/dashboard",
authMiddleware,
dashboardController
);
The execution flow looks like this:
If authentication fails, the controller never executes.
This is one of the reasons middleware is so powerful.
It lets you keep your route handlers clean by separating reusable logic.
π οΈ Built-in Middleware
Some middleware is already included with Express.
You don't need to install anything.
You simply use it.
π¦ express.json()
Parses incoming JSON requests.
app.use(express.json());
Without this middleware:
console.log(req.body);
returns:
undefined
You've already seen this one earlier.
It's probably the first middleware every Express developer uses.
π express.urlencoded()
Parses data submitted from traditional HTML forms.
app.use(
express.urlencoded({
extended: true
})
);
Without it, form data won't appear inside req.body.
π express.static()
Serves static files like:
HTML
CSS
JavaScript
Images
Fonts
app.use(express.static("public"));
Now a file like:
public/logo.png
becomes available at:
http://localhost:3000/logo.png
No route required.
Express serves it automatically.
π¦ Third-Party Middleware
The Express community has built thousands of reusable middleware packages.
Instead of solving common problems yourself, you simply install them.
Some of the most popular ones include:
π‘οΈ Helmet
app.use(helmet());
Adds several HTTP security headers to make your application safer.
π CORS
app.use(cors());
Allows your frontend and backend to communicate even when they're running on different domains or ports.
We'll cover CORS in detail later because it's an important topic on its own.
π Morgan
app.use(morgan("dev"));
Logs every incoming request.
Example output:
GET /users 200 18 ms
POST /login 401 7 ms
Very useful during development.
π Real-World Example β Authentication Middleware
Let's build something you'll see in almost every production Express application.
Suppose certain routes should only be accessible to logged-in users.
Instead of writing authentication logic inside every route, we move it into middleware.
const auth = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
message: "Unauthorized"
});
}
next();
};
Now protecting a route becomes incredibly simple.
app.get(
"/profile",
auth, // auth middleware
(req, res) => {
res.send("Welcome to your profile!");
}
);
Let's see what happens when someone visits this route.
Without an Authorization Header
The request stops immediately.
The route handler never executes.
With a Valid Authorization Header
This is exactly how JWT authentication works in real-world Express applications.
We'll build one from scratch later in this series.
π Middleware Executes in Registration Order
If you have this question in your mind
"In what order does Express execute middleware?"
The answer is simple.
Top to bottom.
Consider this example.
app.use(middlewareA);
app.use(middlewareB);
app.get("/", handler);
A request to / produces:
middlewareA
middlewareB
handler
Express doesn't try to optimize or reorder middleware.
It executes them exactly in the order you register them.
This is why you'll usually see middleware registered near the top of an Express application.
const express = require("express");
const app = express();
app.use(express.json());
app.use(cors());
app.use(morgan("dev"));
// Routes come afterwards...
If you register middleware after your routes, those routes won't pass through it.
Order matters.
π₯ Error Handling Middleware
There's one special type of middleware that behaves differently.
Instead of three parameters:
(req, res, next)
it has four.
(err, req, res, next)
That extra err parameter tells Express:
"This middleware is responsible for handling errors."
Here's a simple example.
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "Internal Server Error"
});
});
Whenever an error is passed to next(error) or thrown inside your application, Express skips all normal middleware and jumps directly to the nearest error-handling middleware.
This keeps your application from crashing and allows you to send a consistent error response to the client.
I'll dedicate an entire article to error handling later in this series because it's a fundamental part of building reliable production APIs.
π§ The One Rule You'll Never Forget
If there's one thing you should remember about middleware, let it be this:
Every middleware must do exactly one of two things:
Call
next()to continue processing the request.Send a response to end the request.
If it does neither, Express has nowhere to go, and the request hangs forever.
Once this simple rule clicks, the entire Express middleware system becomes much easier to understand, and you'll be able to read almost any Express application with confidence.
π€― What Broke When I First Learned Middleware
Forgot next()
Browser loading forever.
Registered middleware after routes
Middleware never executed.
Forgot express.json()
req.body was undefined.
Every Express developer has done these at least once π
π― Key Takeaways
- Express is built around a middleware pipeline.
- Every request follows the same lifecycle.
- Middleware executes in registration order.
-
next()moves execution forward. -
reqcontains incoming data. -
rescontrols outgoing data. - Understanding middleware makes debugging dramatically easier.
π’ Whatβs Next?
In the next article we'll cover:
- Express Router
- Route Parameters
- REST APIs
- CRUD Operations
- Project Structure
- MVC Architecture
Once you understand Router and Middleware together, Express starts feeling ridiculously elegant π
π¬ Your Turn
What confused you most when learning Express?
- Middleware?
- next()?
- req.body?
- Route order?
Let me know in the comments π
π£ Call To Action
If this article helped you:
- β Bookmark it
- π Share it with a fellow developer
- π Build a small Express API today
- π¨βπ» Follow for more Node.js and backend deep dives










Top comments (0)