---
title: A Step-by-Step Guide to Setting Up a Node.js API with Express
siteUrl: https://logzly.com/codecrafthub
author: codecrafthub (CodeCraft Hub)
date: 2026-06-13T09:47:25.843702
tags: [node, express, api]
url: https://logzly.com/codecrafthub/a-step-by-step-guide-to-setting-up-a-node-js-api-with-express
---


You’ve probably heard the buzz about “headless” architectures, micro‑services, and “serverless” functions. The common thread? A solid, lightweight API that talks JSON. If you’re a front‑end developer who’s finally ready to dip a toe into the backend, Express is the friendliest bridge you’ll find. In this post I’ll walk you through creating a minimal Node.js API from scratch, explain why each step matters, and sprinkle in a few of the pitfalls I’ve tripped over in my own projects.

## Why Express Still Matters in 2024

When I first started building APIs, I was juggling a dozen different frameworks, each promising “the next big thing.” Fastify, Koa, Hapi – all great, but each added its own learning curve. Express, by contrast, stays true to the Unix philosophy: do one thing well and let you compose the rest. It’s tiny, it’s battle‑tested, and it integrates seamlessly with the tools we already love (TypeScript, Prisma, Docker, you name it). That stability is why many production services still run on Express, even as serverless platforms rise and as part of broader [performance audits with Lighthouse](/codecrafthub/performance-audits-with-lighthouse-interpreting-scores-and-fixing-issues).

## Prerequisites

Before we dive in, make sure you have:

* **Node.js** (v18 or later) installed – you can verify with `node -v`.
* **npm** or **yarn** – the package manager you’re comfortable with.
* A code editor – VS Code works great for me; its integrated terminal saves a lot of clicks.
* Basic familiarity with JavaScript – if you’re comfortable with `async/await`, you’re good to go.

## 1. Scaffold the Project

Open your terminal and create a new folder:

```bash
mkdir my-express-api
cd my-express-api
npm init -y
```

The `npm init -y` command generates a `package.json` with default values. Feel free to edit the file later – give your project a meaningful name, version, and description.

### Install Core Dependencies

```bash
npm install express
npm install --save-dev nodemon
```

* `express` – the web framework that will handle routing and middleware.
* `nodemon` – a development helper that restarts the server automatically when you change a file.

Add a start script to `package.json`:

```json
"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js"
}
```

Now you can run `npm run dev` to launch the server in watch mode.

## 2. Create the Entry Point

Create a file called `index.js` at the root of the project:

```js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON bodies
app.use(express.json());

// Simple health check route
app.get('/health', (req, res) => {
  res.json({ status: 'OK', timestamp: Date.now() });
});

app.listen(PORT, () => {
  console.log(`🚀 Server listening on http://localhost:${PORT}`);
});
```

A few things to note:

* `express.json()` is built‑in middleware that parses incoming JSON payloads, so you don’t have to manually `JSON.parse` the request body.
* The `/health` endpoint is a tiny but useful convention. Load balancers and monitoring tools love a quick “are you alive?” check.

Run `npm run dev` and visit `http://localhost:3000/health` – you should see a JSON response.

## 3. Organize Routes and Controllers

As your API grows, you’ll want to separate concerns. Let’s build a tiny “todos” resource.

### Folder Structure

```
my-express-api/
│
├─ routes/
│   └─ todos.js
├─ controllers/
│   └─ todosController.js
├─ models/
│   └─ todo.js
└─ index.js
```

### Define a Simple Model

For this guide we’ll keep data in memory. Create `models/todo.js`:

```js
let todos = [];
let idCounter = 1;

module.exports = {
  getAll: () => todos,
  getById: (id) => todos.find(t => t.id === id),
  create: (payload) => {
    const newTodo = { id: idCounter++, ...payload };
    todos.push(newTodo);
    return newTodo;
  },
  update: (id, payload) => {
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) return null;
    todos[index] = { ...todos[index], ...payload };
    return todos[index];
  },
  remove: (id) => {
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) return false;
    todos.splice(index, 1);
    return true;
  }
};
```

This module mimics a tiny data layer. In a real project you’d swap it out for a database client (Prisma, Mongoose, etc.).

### Write Controllers

Create `controllers/todosController.js`:

```js
const Todo = require('../models/todo');

exports.list = (req, res) => {
  res.json(Todo.getAll());
};

exports.get = (req, res) => {
  const todo = Todo.getById(parseInt(req.params.id));
  if (!todo) return res.status(404).json({ error: 'Not found' });
  res.json(todo);
};

exports.create = (req, res) => {
  const { title, completed = false } = req.body;
  if (!title) return res.status(400).json({ error: 'Title is required' });
  const newTodo = Todo.create({ title, completed });
  res.status(201).json(newTodo);
};

exports.update = (req, res) => {
  const updates = req.body;
  const updated = Todo.update(parseInt(req.params.id), updates);
  if (!updated) return res.status(404).json({ error: 'Not found' });
  res.json(updated);
};

exports.remove = (req, res) => {
  const success = Todo.remove(parseInt(req.params.id));
  if (!success) return res.status(404).json({ error: 'Not found' });
  res.status(204).send();
};
```

Each function receives the Express `req` and `res` objects, performs a simple operation, and sends back JSON. Notice the use of HTTP status codes: `201` for created, `204` for no content after deletion, and `400`/`404` for client errors. These conventions help API consumers handle responses predictably.

### Wire Up the Router

Create `routes/todos.js`:

```js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/todosController');

router.get('/', controller.list);
router.get('/:id', controller.get);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.delete('/:id', controller.remove);

module.exports = router;
```

Now modify `index.js` to mount this router:

```js
// ... previous code ...

const todosRouter = require('./routes/todos');
app.use('/api/todos', todosRouter);

// ... rest unchanged ...
```

Restart the server (`npm run dev`) and test the endpoints with a tool like Postman or curl.

## 4. Add Basic Error Handling Middleware

Express lets you define a “catch‑all” error handler. Place it after all routes:

```js
// Global error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
});
```

If any controller throws an exception, this middleware will catch it and return a generic 500 response. In production you’d want more sophisticated logging (Winston, Pino) and perhaps different messages for development vs. production.

## 5. Enable CORS for Front‑End Consumption

Most browsers block cross‑origin requests unless the server explicitly allows them. Install the `cors` package:

```bash
npm install cors
```

Then add it near the top of `index.js`:

```js
const cors = require('cors');
app.use(cors()); // defaults to allowing all origins
```

If you need real‑time updates, consider [integrating WebSockets into your React app](/codecrafthub/integrating-websockets-into-your-react-app-for-realtime-updates). For tighter security you can pass an options object, e.g., `{ origin: 'https://myfrontend.com' }`.

## 6. Environment Variables and Configuration

Hard‑coding values like the port works for demos, but real deployments need flexibility. Create a `.env` file (add it to `.gitignore` later) and install `dotenv`:

```bash
npm install dotenv
```

At the very top of `index.js`:

```js
require('dotenv').config();
const PORT = process.env.PORT || 3000;
```

Now you can set `PORT=8080` in `.env` without touching code.

## 7. Dockerize the API (Optional but Handy)

If you plan to ship this service to a cloud provider, a Docker container gives you consistency across environments.

Create a `Dockerfile`:

```Dockerfile
# Use official Node LTS image
FROM node:18-alpine

# Create app directory
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Start the app
CMD ["node", "index.js"]
```

Build and run:

```bash
docker build -t my-express-api .
docker run -p 3000:3000 my-express-api
```

Your API is now containerized and ready for Kubernetes, ECS, or any host that runs Docker.

## 8. Testing the Endpoints Quickly

Even a handful of unit tests can catch regressions early. Pair that with techniques to [optimize JavaScript load times](/codecrafthub/how-to-optimize-javascript-load-times-for-faster-page-rendering) for a snappier client experience. Install a lightweight testing stack:

```bash
npm install --save-dev jest supertest
```

Add a test file `tests/todos.test.js`:

```js
const request = require('supertest');
const app = require('../index'); // assuming you export the Express app

describe('GET /api/todos', () => {
  it('should return an empty array initially', async () => {
    const res = await request(app).get('/api/todos');
    expect(res.statusCode).toBe(200);
    expect(res.body).toEqual([]);
  });
});
```

Update `package.json`:

```json
"scripts": {
  "test": "jest"
}
```

Run `npm test`. If the test passes, you have a baseline you can extend as you add features.

## 9. Deploying to a Free Tier

For a quick demo, I love using Render or Railway – they spin up a Node environment with a single git push. The steps are basically:

1. Push your repo to GitHub.
2. Connect the repo to the Render service.
3. Set the start command to `npm start`.
4. Add any environment variables (like `PORT`).

Within minutes you’ll have a public URL like `https://my-express-api.onrender.com/api/todos`.

## Wrap‑Up Thoughts

Building a Node.js API with Express is less about memorizing a mountain of configuration and more about understanding the flow: request → middleware → controller → model → response. Once you internalize that pipeline, you can swap out pieces (replace the in‑memory model with a real database, add authentication middleware, or move to TypeScript) without rewriting the whole thing.

I’ve kept this guide deliberately simple; the real world throws in authentication, rate limiting, validation libraries, and async error handling. But the skeleton you’ve just built is a solid foundation. Next time you’re staring at a blank repo, remember: a few lines of Express code can turn that blank canvas into a fully functional API ready for your next React or Vue front‑end.