mosaddik.dev
All posts

How to build a simple REST API with Node.js and Express

Mosaddik Billah 5 min read

JavaScript & React

A laptop on a table showing code in an editor
Photo by James Harrison on Unsplash
On this page
  1. 1. Set up the project
  2. 2. Create the server
  3. 3. Add the routes
  4. 4. Try it
  5. 5. Handle unknown routes and errors
  6. 6. Before you use it for real
  7. Calling your API from Wix

A REST API is a set of URLs that other programs can call to read and change data: a website, a mobile app, or a Wix site sending orders to your own system. Express is the most widely used way to build one with Node.js. In this guide we’ll build a small API for a list of tasks, with routes to list, create, update and delete them.

You’ll need Node.js 20 or newer. Check with node --version.

1. Set up the project#

Shell
mkdir tasks-api
cd tasks-api
npm init -y
npm install express

Open package.json and add "type": "module" so you can use import, plus a script to start the server:

JSON
{
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js"
  }
}

node --watch restarts the server whenever you save a file.

2. Create the server#

Create server.js:

JavaScript
import express from "express";

const app = express();
app.use(express.json());

app.get("/health", (req, res) => {
  res.json({ ok: true });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`API running on http://localhost:${port}`);
});

Run npm run dev and open http://localhost:3000/health. You should see {"ok":true}.

express.json() reads JSON request bodies, so req.body is ready to use in the routes below.

3. Add the routes#

For now the tasks live in an array in memory. They’re lost when the server restarts, which is fine while you learn; step 6 covers what to use instead.

JavaScript
let tasks = [
  { id: 1, title: "Write the brief", done: false },
];
let nextId = 2;

// List all tasks
app.get("/tasks", (req, res) => {
  res.json(tasks);
});

// Get one task
app.get("/tasks/:id", (req, res) => {
  const task = tasks.find((t) => t.id === Number(req.params.id));
  if (!task) return res.status(404).json({ error: "Task not found" });
  res.json(task);
});

// Create a task
app.post("/tasks", (req, res) => {
  const title = req.body?.title?.trim();
  if (!title) return res.status(400).json({ error: "title is required" });

  const task = { id: nextId++, title, done: false };
  tasks.push(task);
  res.status(201).json(task);
});

// Update a task
app.patch("/tasks/:id", (req, res) => {
  const task = tasks.find((t) => t.id === Number(req.params.id));
  if (!task) return res.status(404).json({ error: "Task not found" });

  if (typeof req.body?.title === "string") task.title = req.body.title.trim();
  if (typeof req.body?.done === "boolean") task.done = req.body.done;
  res.json(task);
});

// Delete a task
app.delete("/tasks/:id", (req, res) => {
  const before = tasks.length;
  tasks = tasks.filter((t) => t.id !== Number(req.params.id));
  if (tasks.length === before) return res.status(404).json({ error: "Task not found" });
  res.status(204).end();
});

Put these routes before app.listen. Each one follows the same pattern: read the input, check it, do the work, and reply with the right status code.

Method and URL Does Success code
GET /tasks List tasks 200
GET /tasks/:id Get one task 200
POST /tasks Create a task 201
PATCH /tasks/:id Change some fields 200
DELETE /tasks/:id Delete a task 204

4. Try it#

In a second terminal:

Shell
curl http://localhost:3000/tasks

curl -X POST http://localhost:3000/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Send the invoice"}'

curl -X PATCH http://localhost:3000/tasks/2 \
  -H "Content-Type: application/json" \
  -d '{"done":true}'

curl -X DELETE http://localhost:3000/tasks/1

Try a bad request too, such as a POST with no title. You should get a 400 with a clear message, not a crash.

5. Handle unknown routes and errors#

Add these after all your routes and before app.listen:

JavaScript
// Any URL that no route matched
app.use((req, res) => {
  res.status(404).json({ error: "Not found" });
});

// Any error thrown in a route
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ error: "Something went wrong" });
});

Express knows the second one is an error handler because it takes four arguments. In Express 5, errors thrown inside async route handlers also reach it, so you don’t need a try...catch in every route.

The error handler sends a general message rather than err.message, so details about your code and database don’t leak to callers. Invalid JSON in a request body is reported by Express with status 400, which this handler passes on.

6. Before you use it for real#

  • Store data in a database such as PostgreSQL, MongoDB or SQLite instead of an array. The routes stay almost the same; only the lines that read and write tasks change, and they become await calls (see async and await explained).
  • Check input more strictly. For bigger APIs, a schema library such as Zod describes what each request must contain in one place.
  • Protect it. Anything that changes data should require an API key or a login. Keep secrets in environment variables, never in the code.
  • Limit where it can be called from. If a browser on another domain calls your API, add the cors package and allow only your own sites.
  • Host it on a platform that runs Node.js, such as Render, Railway or Fly.io, and set PORT and your secrets in its dashboard.

Calling your API from Wix#

Once your API is online, a Wix site can call it from backend code with fetch, keeping the API key in the Wix Secrets Manager. How to call an external API from Wix Velo shows the Wix side step by step.

If you need an API built or connected to your Wix site, I do custom API integration.

Comments