Building the Engine of a Backend Application
Understanding the Event Loop, Modules, and Packages
Node.js is fast because it is non-blocking. How does it manage this? With the Event Loop.
Analogy: A busy restaurant kitchen with one very fast chef (the main thread) and many waiters (the Node.js APIs).
We need a way to split our code into logical, reusable files. Node.js has two primary module systems.
This is the original module system in Node.js, using `require` and `module.exports`.
// logger.js
function log(message) {
console.log(`[LOG]: ${message}`);
}
module.exports = { log };
// app.js
const logger = require('./logger.js');
logger.log('Server started.');
This is the standard used in modern JavaScript and browsers, using `import` and `export`.
// logger.mjs (note the file extension)
export function log(message) {
console.log(`[LOG]: ${message}`);
}
// app.mjs
import { log } from './logger.mjs';
log('Server started.');
You must either use the `.mjs` extension or set `"type": "module"` in your `package.json` to enable ESM.
NPM is the world's largest software registry. It's how we download and manage third-party tools (packages) for our projects.
Building a Scalable Server Architecture
As your app grows, putting all your routes in one file becomes messy. `express.Router` is a mini Express application, allowing you to group related routes into their own files.
`routes/users.js`
const express = require('express');
const router = express.Router();
// This route is actually /users/
router.get('/', (req, res) => {
res.send('List of all users');
});
// This route is actually /users/:id
router.get('/:id', (req, res) => {
res.send(`Details for user ${req.params.id}`);
});
module.exports = router;
In your main server file, you import the router and tell Express to use it for a specific path.
`app.js`
const express = require('express');
const app = express();
const userRoutes = require('./routes/users.js');
// Any request starting with /users will be handled by userRoutes
app.use('/users', userRoutes);
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
This creates a clean, scalable file structure.
Middleware functions are the heart of Express. They are functions that have access to the request object (`req`), the response object (`res`), and the `next` function in the application’s request-response cycle.
Analogy: An assembly line. Each middleware is a station that can inspect the request, modify it, or end the cycle, before passing it to the next station with `next()`.
Let's write a simple middleware that logs every incoming request.
const requestLogger = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
// VERY IMPORTANT: Call next() to pass control to the next middleware
next();
};
// Use it in your app
app.use(requestLogger);
// Now, every request will be logged to the console!
app.get('/', (req, res) => {
res.send('Hello World!');
});
You don't have to write everything yourself. The Express ecosystem is rich with powerful third-party middleware.
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors()); // Allow requests from other origins
app.use(express.json()); // Alows us to read `req.body`
By default, an error in Express might crash your server. A professional application needs a centralized way to catch and handle errors.
Express has a special type of middleware for this. It has four arguments: `(err, req, res, next)`. It MUST be defined last, after all other `app.use()` and route calls.
// ... all your routes and other middleware ...
// The Error Handler
app.use((err, req, res, next) => {
console.error(err.stack); // Log the error for the developer
// Send a generic, safe response to the client
res.status(500).send('Something broke!');
});
Your task is to structure a simple Express API with separate route files and custom middleware.
/my-blog-api
├── node_modules/
├── routes/
│ └── posts.js (Contains the express.Router)
├── app.js (Main server file)
└── package.json
const timestamp = (req, res, next) => {
req.requestTime = new Date().toISOString();
next();
};
app.use(timestamp);
router.get('/', (req, res) => {
res.send(`All posts requested at: ${req.requestTime}`);
});