How to build a simple REST API with Node.js and Express
Mosaddik Billah 5 min read

On this page
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#
mkdir tasks-api
cd tasks-api
npm init -y
npm install expressOpen package.json and add "type": "module" so you can use import, plus a script to start the server:
{
"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:
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.
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:
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/1Try 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:
// 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
taskschange, and they becomeawaitcalls (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
corspackage and allow only your own sites. - Host it on a platform that runs Node.js, such as Render, Railway or Fly.io, and set
PORTand 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