How to Build a Simple REST API With Node.js and Express
Building your first REST API is the moment you stop consuming other people's data and start serving your own, and Express is still the most direct way to learn it in Node.js because it hides very little. A REST API is a web service that exposes resources, such as tasks, users, or products, at predictable URLs and lets clients read and change them using standard HTTP methods, typically exchanging JSON. This guide builds a small but properly structured task API, the kind you could hand to a front-end developer without embarrassment, and explains the reasoning behind each decision.
Quick Answer: To build a simple REST API with Express, initialize a Node.js project, install Express, enable JSON parsing with express.json(), define routes for each resource using GET, POST, PATCH, and DELETE, validate incoming data, return correct status codes, and add a central error-handling middleware. Test every endpoint with curl or an API client before connecting a database.
What Makes an API RESTful?
An API is RESTful when it models data as resources at stable URLs and uses HTTP methods and status codes for their standard meanings. You do not need to memorize the original academic definition to build a good one; a few consistent conventions get you most of the way.
A resource is a noun, such as tasks. Its collection lives at /tasks and each item at /tasks/:id. The method expresses the action: GET reads, POST creates, PATCH partially updates, PUT replaces, and DELETE removes. That is why REST URLs should not contain verbs like /getTasks or /deleteTask. If the client side of this conversation is still new to you, start with this explanation of what an API actually is and how websites call one.
REST APIs are also stateless: every request carries everything the server needs to process it, such as an authentication token, rather than relying on server memory of previous requests. Statelessness makes APIs easier to scale and to cache.
Express is a minimal Node.js web framework built around two ideas: routes, which match a method and path to a handler, and middleware, which are functions that run in order on every request and can modify it, end it, or pass it along with next(). Express 5, the current major version, also forwards errors from rejected promises in async handlers to your error middleware automatically, which removes a lot of boilerplate older tutorials needed.
Planning the Endpoints Before Writing Code
Planning endpoints on paper first produces cleaner APIs than designing them in the editor. For a task manager, the full set of operations looks like this:
- GET /api/tasks returns all tasks, optionally filtered with a query string such as
?done=true. - GET /api/tasks/:id returns one task, or 404 if it does not exist.
- POST /api/tasks creates a task from a JSON body and returns 201 with the new task.
- PATCH /api/tasks/:id updates only the fields sent, such as marking a task done.
- DELETE /api/tasks/:id removes a task and returns 204 with no body.
- GET /api/health returns a simple status object, useful for hosting platforms and uptime monitors.
Decide your data shape at the same time. A task might have an id, a title string, a done boolean, and a createdAt timestamp. Writing this down tells you exactly what to validate and keeps responses consistent across endpoints. Prefixing routes with /api also helps if the same server later serves a front end, and adding a version such as /api/v1 gives you room to change the design without breaking existing clients.
Choosing the Right Status Codes
Correct status codes let clients handle responses without parsing error messages, which is the difference between an API that is pleasant to use and one that is not. These are the codes a simple CRUD API needs.
| Situation | Status Code | Response Body |
|---|---|---|
| Successful read or update | 200 OK | The requested or updated resource |
| Resource created | 201 Created | The new resource, with its id |
| Successful delete | 204 No Content | Empty |
| Invalid input | 400 Bad Request | An error object explaining which fields failed |
| Resource not found | 404 Not Found | An error object with a short message |
| Unexpected server failure | 500 Internal Server Error | A generic message, with details only in logs |
Pick one error format and use it everywhere, for example { "error": { "message": "Title is required" } }. Front-end code can then display errors with a single function instead of special cases for each endpoint.
Why Validation and Error Handling Matter More Than Routes
Routes are the easy part of an API; validation and error handling are what keep it reliable once real clients use it. This section is practitioner analysis from reviewing many beginner APIs.
The most common flaw I see is trusting req.body. Without validation, a client can send a title that is a number, an empty string, or a ten-megabyte paragraph, and the API stores it happily. Later, the front end crashes on data it never expected. Validate type, presence, and length on every write. Small schema libraries such as Zod make this declarative, but a few explicit checks are fine for a first API. Also note that express.json() accepts a size limit option, which protects you from oversized payloads.
The second flaw is leaking internals. When an unhandled error occurs, beginner APIs often return the full stack trace, which reveals file paths and library versions to anyone. A single error-handling middleware, registered after all routes, should log the details and return a generic 500 message.
The third flaw is having no authentication on write endpoints. For learning projects that is acceptable, but before deploying an API that changes data, protect it. The session-based approach in this beginner Node.js login system guide works well when the API and front end share a domain.
Common Mistakes When Building an Express API
Most Express API bugs come from a short list of predictable mistakes. Avoid these and your first API will already be more robust than many tutorials.
- Forgetting express.json(). Without it,
req.bodyis undefined for JSON requests. Register it before your routes. - Returning 200 for everything. Clients cannot tell success from failure without parsing text. Use the status codes in the table above.
- Sending two responses. Calling
res.json()twice throws "Cannot set headers after they are sent." Usereturn res.status(404).json(...)to stop execution after early responses. - Registering error middleware too early. Express identifies error handlers by their four arguments and runs them in order. Place yours after every route.
- Using verbs in URLs. Prefer
DELETE /tasks/5overGET /deleteTask?id=5. GET requests should never change data. - Opening CORS to everyone in production. Allowing every origin is convenient locally but risky later. Configure the cors package with your actual front-end origin.
- Hardcoding the port and secrets. Read
process.env.PORTand secrets from environment variables, because hosting platforms assign ports dynamically. - Keeping data only in memory. An in-memory array is perfect for learning, but it resets on every restart. Plan the move to a database.
Walkthrough: Building the Task API
This walkthrough builds the task API end to end with an in-memory store, structured so a database can replace it later. You need a recent Node.js LTS release.
Step 1: Set up the project. Run npm init -y, then npm install express. In package.json, set "type": "module" to use import syntax, and add a dev script using Node's built-in watch mode: "dev": "node --watch server.js".
Step 2: Create the server. In server.js, import Express, create the app, add app.use(express.json({ limit: '100kb' })), and listen on process.env.PORT || 3000.
Step 3: Add the routes. Keep tasks in an array and a counter for ids while learning:
app.post('/api/tasks', (req, res) => {
const title = typeof req.body.title === 'string' ? req.body.title.trim() : '';
if (!title || title.length > 200) {
return res.status(400).json({ error: { message: 'Title must be 1-200 characters' } });
}
const task = { id: nextId++, title, done: false, createdAt: new Date().toISOString() };
tasks.push(task);
res.status(201).json(task);
});Step 4: Implement read, update, and delete. For routes with :id, convert the parameter with Number(req.params.id), find the task, and return 404 if it is missing. In PATCH, update only fields that are present and valid, such as a boolean done.
Step 5: Add a 404 handler for unknown routes. After all routes, add middleware that returns 404 with your standard error format.
Step 6: Add error-handling middleware. Register a function with four arguments, (err, req, res, next), that logs the error and responds with a generic 500 message.
Step 7: Test with curl. Create a task with curl -X POST localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title":"Write docs"}', then list, update, and delete it. Also send an empty title and a nonexistent id to confirm 400 and 404 responses.
Step 8: Separate routes from data access. Move array operations into a small module with functions like listTasks and createTask. When you add PostgreSQL or SQLite later, only that module changes, and every route keeps working.
Step 9: Add production safeguards. Before deploying, install a few small, well-known middleware packages. Helmet sets sensible security-related HTTP headers with one line of setup. The cors package, configured with your real front-end origin, controls which browsers may call the API. A rate limiter such as express-rate-limit on write endpoints stops a single client from flooding your server. Add basic request logging so you can see method, path, status code, and response time for every request; when a front-end developer reports a bug, those logs answer most questions in seconds. Finally, write a short README listing each endpoint, its expected body, and example responses, because an API without documentation forces every client to read your source code.
Key Takeaways
- A RESTful API models data as resources at noun-based URLs and uses HTTP methods and status codes for their standard meanings.
- Planning endpoints and the data shape before coding produces consistent routes and clear validation rules.
- Correct status codes such as 201, 204, 400, and 404 let clients handle responses without parsing messages.
- Validating request bodies and using one central error handler prevent most reliability and security problems.
- Separating data access from routes lets you swap an in-memory store for a database without rewriting the API.
Frequently Asked Questions
Is Express still a good choice for REST APIs?
Yes, Express remains a solid, widely used choice for REST APIs, especially for learning, because it is minimal and well documented. Express 5 modernized error handling for async code. Alternatives like Fastify or Hono offer different performance or runtime features, but the REST concepts you learn transfer directly.
What is the difference between PUT and PATCH?
PUT replaces an entire resource with the data you send, so missing fields may be cleared. PATCH applies a partial update, changing only the fields included in the request. For most simple APIs, PATCH is the more practical choice for edits like toggling a task's done status.
How do I test my Express API without a front end?
Use curl from the terminal, or an API client such as Postman, Insomnia, or the REST Client extension in VS Code. For automated tests, libraries like Supertest let you send requests to your Express app inside a test runner and assert on status codes and response bodies.
Do I need a database to build a REST API?
Not to learn. An in-memory array lets you focus on routes, validation, and status codes. Data disappears on restart, though, so any real API needs a database. Keeping data access in a separate module makes switching to SQLite, PostgreSQL, or MongoDB straightforward later.
How do I fix CORS errors with my Express API?
Install the cors package and register it as middleware before your routes, configured with the exact origin of your front end. Browsers block cross-origin requests unless the server sends permission headers. Avoid allowing every origin in production, especially for endpoints that change data.
Where can I deploy a Node.js and Express API?
You can deploy an Express API to application platforms such as Render or Railway, to a VPS you manage, or adapt it to serverless functions on platforms like Vercel. Make sure the app reads the port from process.env.PORT and stores secrets in environment variables.
Conclusion
The most important lesson in building a REST API is that consistency beats cleverness: noun-based routes, correct status codes, validated input, and one error format make an API predictable for every client that uses it. Build the in-memory version first, test each endpoint with curl, then swap in a database behind a clean data module. When it works locally, compare these free hosts that can run your Express server and put your API online.




