Backend Series: Express.js and MongoDB – The Scalable Tech Stack Explained


Welcome back to the backend development series!
In the previous posts, we built basic web servers using Node.js and Bun to understand how raw server handling works. But let’s face it — writing vanilla backend logic using plain HTTP methods quickly becomes messy and unscalable.
So what’s the next logical step?
Using frameworks and libraries that improve developer experience, scalability, and code maintainability.
That’s exactly what this post is about. We’ll explore:
Why we chose Express.js for routing and logic
How MongoDB and Mongoose fit into the stack
Alternative backend frameworks and ORMs (like Hono, Prisma, Drizzle)
How all components work together in a scalable architecture
Let’s begin.
Why Use a Framework Like Express?
So far, you’ve seen how we can manually build web servers using vanilla Node.js or Bun — and yes, it works!
You can handle routes like /
, /ice-tea
, and even return custom responses.
But here’s the catch:
👉 What happens when your app grows to 20, 50, or 100 routes?
You’ll find yourself buried in a spaghetti mess of if...else if...else
or switch-case
blocks.
Not only is it painful to write, but it’s also:
❌ Hard to read
❌ Difficult to debug
❌ Not scalable
❌ Nearly impossible to test cleanly
That’s where Express.js comes in.
What is Express?
Express.js is a fast, unopinionated, and minimalist web framework for Node.js. In simple terms, it's a lightweight layer built on top of Node.js's native HTTP module that simplifies the process of building web servers and APIs.
You don’t need to manually:
Check
req.url
Set status codes and headers
Use
res.end()
Instead, Express lets you define clean, modular routes, manage middlewares, and handle errors gracefully — all with far fewer lines of code.
Why Choose Express?
Benefit | Description |
✅ Fast to learn | Its minimalist design makes it easy for beginners to get started quickly. The core concepts of routing and middleware are straightforward and intuitive. |
✅ Minimal & flexible | Express is "unopinionated," meaning it doesn't force a specific project structure or design pattern on you. This gives developers complete control to build applications their own way, adding only the components they need. |
✅ Massive ecosystem | Its biggest strength is its vast collection of middleware modules available on npm. Need authentication, logging, or file parsing? There's likely a well-tested middleware package for it, which significantly speeds up development. |
✅ Production-ready | Express is battle-tested and used in production by major companies like Uber, IBM, and Accenture. Its stability and strong community support mean that most common problems have already been solved and documented. |
✅ Strong community support | Tons of tutorials, Stack Overflow threads, GitHub issues already solved |
What Does Express Look Like?
The elegance of Express lies in its simplicity. Compare building a simple server in raw Node.js with the clean syntax of Express.
Here’s how you define routes using Express:
const express = require("express");
const app = express();
const port = 3000;
// Defines a route for the root URL ("/")
app.get("/", (req, res) => {
res.send("Hello, it's Ice Tea!");
});
// Defines another route for "/ice-tea"
app.get("/ice-tea", (req, res) => {
res.send("Ice tea is a good option!");
});
// A simple 404 handler for any other route
app.use((req, res) => {
res.status(404).send("404 - Not Found");
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
if/else
chains required in plain Node.js. Routes are clearly defined with HTTP methods (app.get
), and sending a response is as simple as res.send()
. This structure makes code easier to read, maintain, and scale for real-world applications.Using plain Node.js or Bun gives you control, but that control comes at a cost:
More boilerplate. More clutter. More mental overhead.
Let's break down what each part does.
1. Initialization
const express = require("express");
const app = express();
const port = 3000;
const express = require("express");
: This line imports the Express framework into our file. Therequire
function is Node.js's way of including external modules.const app = express();
: Here, we create an instance of the Express application by calling theexpress()
function. Thisapp
object is the core of our server and we'll use it to define routes and configure our server.const port = 3000;
: This line defines a constant variable to hold the port number our server will run on. Using a variable makes it easy to change the port later if needed.
2. Defining Routes
Routes determine how the application responds to a client request at a specific URL (or path) and with a specific HTTP method (like GET, POST, etc.).
// Defines a route for the root URL ("/")
app.get("/", (req, res) => {
res.send("Hello, it's Ice Tea!");
});
// Defines another route for "/ice-tea"
app.get("/ice-tea", (req, res) => {
res.send("Ice tea is a good option!");
});
app.get(...)
: This method tells the server to respond only to HTTP GET requests."/"
: This is the path for the first route, representing the root URL of the site (e.g.,http://localhost:3000
). The second route is for the/ice-tea
path.(req, res) => { ... }
: This is the handler function that Express executes when a request matches the path.req
(Request): An object containing information about the incoming HTTP request, such as headers or query parameters.res
(Response): An object used to send a response back to the client.res.send(...)
: A simple Express method to send a response back to the client. Here, it sends a plain text string.
3. Handling 404 Errors (Middleware)
// A simple 404 handler for any other route
app.use((req, res) => {
res.status(404).send("404 - Not Found");
});
app.use(...)
: This method applies a middleware function. Middleware are functions that run during the request-response cycle.How it works: Express processes routes and middleware in the order they are defined. If a request comes in that doesn't match
/
or/ice-tea
, it "falls through" to thisapp.use()
handler.res.status(404)
: This sets the HTTP status code of the response to 404 (Not Found)..send(...)
: This sends the message "404 - Not Found" as the response body.
4. Starting the Server
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
app.listen(...)
: This crucial method starts the server and makes it listen for incoming connections on the specified port.port
: The first argument is the port number we defined earlier (3000).() => { ... }
: The second argument is a callback function that executes once the server has successfully started. Here, it logs a message to the console to let us know that the server is up and running and where to access it.
The Architecture: Request → Logic → Database
Before we dive into building full-fledged APIs, let’s understand how everything connects in a real-world backend setup.
Here’s a simplified breakdown of the modern backend flow:
Client (Browser / Postman / Mobile App)
↓
Node.js + Express Server
↓
Business Logic / Auth Layer
↓
Mongoose (MongoDB ORM)
↓
MongoDB Database
Client
Can be a browser, mobile app, or tools like Postman
Sends an HTTP request (GET, POST, etc.) to the server
Think of this as the user saying: "Hey server, I want some data!"
Node.js + Express
Acts as the web server — receives requests and decides how to handle them
Express makes routing and request handling much simpler and readable
Business Logic / Auth Layer
This is where all the core logic happens
Examples:
Authenticating users
Validating form data
Applying business rules before accessing the database
Mongoose (ORM)
Acts as the middle layer between your code and MongoDB
Translates JavaScript objects into MongoDB documents (and vice versa)
Helps you define schemas, apply validation, and write queries easily
MongoDB
A NoSQL database where the actual data is stored
Stores data in collections and documents, not tables and rows
What Is Mongoose?
MongoDB is Great, But Mongoose Makes It Better. Here's How
When our team builds applications with Node.js, we often choose MongoDB as our database. We value its flexibility and performance. However, that same flexibility, if not managed carefully, can lead to inconsistencies in our data, making the application harder to maintain as it grows.
This is where we bring in Mongoose.
Mongoose is an Object Data Modeling (ODM) library that acts as a crucial layer between our application and the MongoDB database. It helps us represent the data in our MongoDB collections as clean, predictable JavaScript objects. It doesn't replace MongoDB, but rather enhances how we interact with it, creating a more structured and robust development process for our team.
1. Establishing Our Data Blueprint with Schemas
While MongoDB is schema-less, for our applications to be scalable and maintainable, we need a predictable structure for our data. Mongoose allows us to enforce this structure by defining Schemas.
A Schema acts as the blueprint for our data. It's a contract that our team agrees upon, defining the expected fields, their data types, and any default values. This ensures every document in a collection is consistent.
// This is the blueprint our team defines for a 'User'
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true },
createdAt: { type: Date, default: Date.now }, // We let Mongoose handle the default value
role: { type: String, default: 'user' }
});
By establishing this schema, we guarantee that any new user document added to our database will conform to this agreed-upon structure, which prevents data-related bugs down the line.
2. Protecting Our Data's Integrity with Validation
Having a blueprint is the first step. Next, we need a reliable way to enforce the rules of that blueprint. We can't risk invalid or incomplete data being saved to our database.
This is where we leverage Mongoose's powerful Validation capabilities. We build validation rules directly into our schema, which acts as the first line of defense for our data's integrity.
// In our product schema, we add validation rules
const productSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: Number, min: 0, required: true }, // We ensure price is never negative
onSale: { type: Boolean, default: false }
});
Now, if a part of our application attempts to save a product with a negative price, Mongoose will reject the operation and throw a validation error. This protects our database from containing illogical data and ensures all developers on our team are working with a clean and reliable dataset.
3. Automating Our Business Logic with Middleware Hooks
In our applications, we often have logic that needs to run automatically during a document's lifecycle. A critical example is hashing a user's password before saving it to the database.
Instead of writing this hashing logic every time we create or update a user, we use Mongoose's Middleware, also known as "hooks." These are functions we configure to run before (pre
) or after (post
) specific events like save
.
// We use a 'pre-save' hook to automatically hash passwords
userSchema.pre('save', async function(next) {
// 'this' refers to the document being saved
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next(); // We call next() to proceed with the save operation
});
By implementing this hook, we encapsulate critical business logic in one place. It runs automatically, ensuring no one on our team has to remember to perform this security step manually. This makes our codebase cleaner, less error-prone, and more secure.
4. Writing Cleaner, More Readable Queries
So far, we've discussed how Mongoose helps us manage data going in. It's just as valuable for how we get data out. While MongoDB's native query language is effective, its syntax can become complex.
Mongoose provides us with a Query Abstraction layer that makes our database queries more intuitive, readable, and easier to maintain. It offers chainable methods that flow logically.
Native MongoDB Driver Example:
db.collection('articles').find({ status: 'published', likes: { $gt: 100 } }).sort({ createdAt: -1 });
How We Write it with Mongoose:
Article.find({ status: 'published' }).where('likes').gt(100).sort('-createdAt');
For us, the Mongoose version is significantly easier to read and debug. It allows us to construct complex queries programmatically in a way that feels like a natural extension of JavaScript, boosting our development speed and code clarity.
In Conclusion:
Ultimately, we choose to use Mongoose because it provides a robust framework for data modeling, validation, and business logic automation. It allows us to harness the power and flexibility of MongoDB while enforcing the structure and discipline necessary to build large-scale, maintainable applications. The small amount of initial setup pays massive dividends in data integrity, code quality, and our team's overall productivity.
Other Tools in the Ecosystem
Category | Our Choice | Other Popular Tools |
Web Framework | Express.js | Hono, Fastify, Aleph.js |
Database | MongoDB | PostgreSQL, MySQL |
ORM / ODM | Mongoose | Prisma, Drizzle, TypeORM |
Web Frameworks: Express.js vs. The Moderns
The choice of a web framework defines how you build the server-side logic of an application.
Express.js: This is the de-facto standard for Node.js. Its biggest strengths are its stability, massive ecosystem of middleware, and minimalist approach. It doesn't impose strict rules, making it incredibly flexible and a perfect, predictable tool for learning core backend concepts.
Hono, Fastify, Aleph.js: These represent more modern alternatives, each with a specific focus.
Hono: Known for being lightweight and incredibly versatile. It's not tied to Node.js and can run on various JavaScript runtimes like Bun, Deno, and edge computing platforms (like Cloudflare Workers).
Fastify: Built with a primary focus on speed and low overhead. It's designed to be one of the fastest Node.js frameworks available.
Aleph.js / Bun: These are on the cutting edge, leveraging new, high-performance runtimes like Bun. They offer a fresh take on tooling and speed but are less mature than Express.
The Verdict: We use Express.js because it's a stable, battle-tested foundation. The concepts learned with Express are easily transferable to other frameworks.
Data Layers: Mongoose vs. Prisma vs. Drizzle
This layer, an ORM (Object Relational Mapper) or ODM (Object Data Modeling) tool, determines how your application talks to your database.
Mongoose: This is an ODM built specifically and exclusively for MongoDB. Its design philosophy is deeply integrated with MongoDB's document-based structure (schemas, middleware, population). If you're committed to MongoDB, Mongoose feels like a natural extension of the database itself.
Prisma: A next-generation ORM that is often praised for its excellent developer experience. Its key features are a declarative schema file and a fully type-safe auto-generated client. While it's primarily used with SQL databases (PostgreSQL, MySQL), it also has support for MongoDB. It's known for making database queries intuitive and safe.
Drizzle: A newer, lightweight ORM that is gaining rapid popularity. Its main advantage is its "TypeScript-first" approach, allowing you to write queries that feel very close to raw SQL while being fully type-safe. It's often chosen by developers who want maximum performance and type safety without the level of abstraction that Prisma has.
The Verdict: We use Mongoose because our database of choice is MongoDB, and Mongoose is tailor-made for it. Once you master the concepts of modeling and querying data with Mongoose, transitioning to a tool like Prisma or Drizzle for a SQL-based project becomes much simpler.
We’re sticking with Express + Mongoose for this course.
Once you're confident, switching to Prisma or Drizzle is a breeze.
Subscribe to my newsletter
Read articles from Santwan Pathak directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Santwan Pathak
Santwan Pathak
"A beginner in tech with big aspirations. Passionate about web development, AI, and creating impactful solutions. Always learning, always growing."