📖 This post is also published on Medium and dev.to.

Read on Medium Read on dev.to

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 GET request to http://localhost:3000/api/users
  • Send a POST with a JSON body: {"name":"Omar","email":"test@test.com"}
  • Check the response status codes — 200 for GET, 201 for 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".

Omar Faruk Khan

Omar Faruk Khan

Software Engineer · Full Stack & Mobile · Based in Bangladesh. Writing about things I build and learn.