Why REST APIs still matter
REST APIs are the backbone of almost every modern web and mobile application. Whether you're building a backend for a React frontend, a Flutter mobile app, or integrating with third-party services, you'll be writing and consuming REST APIs constantly. Getting the fundamentals right from the start saves a lot of pain later.
In this post I'll walk through building a clean, production-ready API with Node.js and Express — the stack I use most often in my own projects.
Project setup
Start by initialising a new Node.js project and installing the core dependencies:
mkdir my-api && cd my-api
npm init -y
npm install express dotenv
npm install --save-dev nodemon
Create an index.js entry point. The structure I use for most projects looks like this:
my-api/
├── src/
│ ├── routes/
│ │ └── users.js
│ ├── controllers/
│ │ └── userController.js
│ ├── middleware/
│ │ └── errorHandler.js
│ └── app.js
├── .env
└── index.js
Routing and controllers
Separating routes from controllers keeps your code clean and testable. The route file defines what the endpoint is; the controller defines what happens when it's hit.
// src/routes/users.js
const express = require('express');
const router = express.Router();
const { getUsers, createUser } = require('../controllers/userController');
router.get('/', getUsers);
router.post('/', createUser);
module.exports = router;
// src/controllers/userController.js
exports.getUsers = async (req, res, next) => {
try {
res.json({ users: [] });
} catch (err) {
next(err);
}
};
exports.createUser = async (req, res, next) => {
try {
const { name, email } = req.body;
res.status(201).json({ message: 'User created', name, email });
} catch (err) {
next(err);
}
};
Error handling
A global error handler middleware catches anything passed to next(err).
This way you avoid writing the same try/catch response logic everywhere.
// src/middleware/errorHandler.js
module.exports = (err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error',
});
};
Always pass errors to
next(err)instead of sending a response directly inside catch blocks. It gives you one place to control all error output.
Testing with Postman
Once your server is running (npm run dev), open Postman and:
- Send a
GETrequest tohttp://localhost:3000/api/users - Send a
POSTwith a JSON body:{"name":"Omar","email":"test@test.com"} - Check the response status codes —
200for GET,201for created
What's next
This covers the basics. In a follow-up post I'll add input validation with Joi, authentication with JWT, and rate limiting. Those three additions take an API from "it works locally" to "it's safe to deploy".