Introduction to Node.js: Build a Todo API Step by Step
Updated on December 03, 2025 10 minutes read
When you first get into web development, front-end work usually feels more approachable than backend. You can tweak some HTML or CSS, refresh the browser, and instantly see the result. That quick feedback loop is incredibly helpful when you are still getting comfortable with code.
Backend development often feels harder at the beginning. You install tools, configure environments, and run commands long before you see anything visible. Node.js helps bridge that gap by letting you reuse JavaScript on the server to build powerful applications with a familiar language and tooling ecosystem.
In this guide, you will build a small but realistic Todo List API using Node.js, Express, and LowDB. Along the way, you will see how requests travel through your application, how data is stored, and how a simple backend project is structured in 2026.
What we will build
Todo apps are the classic first project in front-end development. They are small enough to understand quickly, but flexible enough to show real concepts like state, validation, and data persistence.
Here, you will build a Todo list REST API. Instead of clicking buttons in a browser, you will expose endpoints to create, read, update, and delete todo items. These todos will be stored in a lightweight JSON-based database so that they survive server restarts.
You will be able to call these endpoints using a tool like Postman or your favourite API client. The final code can be found in the starter repository on GitHub
Our tech stack
You will use a deliberately simple stack. It mirrors common patterns in production systems but keeps the number of tools small so you can focus on concepts instead of configuration.
Node.js
Node.js is a JavaScript runtime that lets you run JavaScript outside the browser. It is built on Chrome's V8 engine and provides APIs for working with files, networking, and the operating system.
Because Node.js uses JavaScript, you can reuse much of what you already know from front-end development. In 2026, it is still one of the most widely used technologies for building web APIs, real-time services, and developer tools.
Express.js
Express.js is a minimalist web framework built on top of Node.js. It handles common server tasks like routing, parsing request bodies, and handling errors, so you can focus on your application's logic.
Express is unopinionated, which means you can structure your project in the way that best fits your use case. In this article, you will use a basic layered structure that is similar to what you would find in many real-world applications.
LowDB
LowDB is a simple JSON-based database. Data is stored in a local file rather than a separate database server, which keeps things easy to understand when you are learning.
It is not designed for high-traffic production systems, but it is great for workshops, prototypes, and learning exercises. The low overhead lets you focus on how your code interacts with a database without worrying about deployments or infrastructure.
Now that you know the tools, you can set up the environment and get the starter project running.
Setting up your environment
Installing Node.js and npm
Node.js is available on major platforms including Windows, macOS, and Linux. You can download the installer for your operating system from the official website:
https://nodejs.org/en/download/
During installation on Windows, make sure the option to add Node.js to your system PATH is enabled. That allows you to run node and npm directly from the terminal.
npm (Node Package Manager) is installed automatically with Node.js and is the standard way to install and manage packages in Node projects. If you want more detail, you can also follow the official npm guide for installing Node.js and npm.
Getting the starter project
Open your terminal and clone the starter repository:
git clone https://github.com/Zineeddine998/workshop_todo_application.git
cd workshop_todo_application
This repository contains a minimal file structure and the dependencies you will need. You will walk through the most important parts as you modify them.
Install the dependencies with:
npm install
This reads the package.json file in the root of the project. That file stores metadata about your project, such as its name, version, dependencies, and npm scripts.
Running the development server
Once the installation is complete, start the application:
npm run start
Behind the scenes, the start script defined in package.json tells Node.js which file should be executed to boot the app. In this project, that entry file is app.js.
If everything works, you should see a message in your terminal similar to:
Listening on port 3000
Open your browser and visit:
http://localhost:3000/
You should see a simple Hello, world! response. That means your Express server is running locally and is ready to handle requests.
Understanding app.js
The app.js file is the main entry point of your project. At a high level, it:
- Creates an Express application.
- Registers middleware to parse JSON requests.
- Defines at least one route, such as a basic home route.
- Starts the server on a given port.
A simplified version of app.js might look like this:
import express from 'express';
import './db.js'; // ensure database is initialised
import postsRouter from './routes/index.js';
const app = express();
const PORT = 3000;
app.use(express.json());
// Root route
app.get('/', (req, res) => {
res.send('Hello, world!');
});
// Posts API
app.use('/posts', postsRouter);
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
The important idea is that each route is mapped to a handler function. When a client sends a request that matches a method and path, Express calls the corresponding function and sends its response back.
Configuring the database with LowDB
Next, you will see how to persist data using LowDB. Instead of running a separate database server, you will read and write to a local db.json file.
Create or open a db.js file and add something similar to:
import { join } from 'path';
import { Low } from 'lowdb';
import { JSONFile } from 'lowdb/node';
const file = join(process.cwd(), 'db.json');
const adapter = new JSONFile(file);
const db = new Low(adapter);
await db.read();
// Initialise the data structure if the file is empty
db.data ||= { posts: [] };
await db.write();
export default db;
Here is what is happening, step by step:
joinfrom Node'spathmodule helps you safely build a cross-platform file path.JSONFiletells LowDB to use a JSON file as its storage backend.db.read()loads the existing contents ofdb.jsoninto memory.db.data ||= { posts: [] }ensures there is at least apostsarray in the data.db.write()saves the initial structure back to disk.
Because reading from disk can take some time, you use await to make sure the file is read before the app starts using the database.
How requests flow through your API
Before writing more code, it is useful to understand the typical flow of a request in a Node.js and Express application.
At a high level, the architecture looks like this:
- A client sends an HTTP request to your server.
- A router decides which controller should handle that request.
- The controller calls service functions that implement business logic.
- The services interact with the database layer to read or modify data.
- A response is constructed and sent back to the client.
HTTP Request Layer
Think of this as the gateway to your application. It receives requests coming from different clients and forwards them to the right place.
Routers: In Express, routers let you group logically related routes together. For example, all routes starting with /posts can live in one router. This keeps your code decoupled, easier to navigate, and simpler to extend.
Controllers: A controller receives a request from a router, performs basic validation or preprocessing, and passes the heavy lifting to a service function. It then formats the response and chooses the appropriate HTTP status code.
Business Logic Layer
This layer contains the actual rules of your application.
Services: Service functions implement the core logic. They take parameters from controllers, perform calculations or validations, talk to the database, and return results or errors.
Third-party services: Many real-world apps use external APIs for payments, file storage, notifications, or analytics. In a larger project, those integrations are often also encapsulated in service modules.
ORM or ODM (optional): In larger systems, an Object-Relational Mapper (ORM) or Object-Document Mapper (ODM) can sit between services and the database. It provides a higher-level abstraction over raw queries. In this tutorial, LowDB already lets you work directly with JavaScript objects, so you do not need an additional ORM.
Data Persistence Layer
Finally, your data ends up in a database.
Databases: This is where your todos and other entities are stored. Different projects choose different database types, such as relational, document, key-value, or graph, depending on their scalability and consistency needs. For this project, LowDB gives you a convenient, file-based store.
Understanding this flow makes it easier to switch between frameworks later. Whether you use Node.js, Python, or another language, the same ideas of routers, controllers, services, and persistence tend to show up again.
Implementing add todo end to end
Now you will walk through one feature across all layers: adding a new todo item, also called a post, to the list.
You will assume that each todo has:
- a unique
id - a
titlestring - an
orderinteger indicating its position
1. Defining the router
Create a routes/index.js file and add:
import { Router } from 'express';
import { addPost } from '../controllers/postsController.js';
const router = Router();
router.post('/', addPost);
export default router;
This tells Express that whenever a POST request hits /posts, it should call the addPost controller function. You already connected this router in app.js with app.use('/posts', postsRouter);.
2. Writing the controller
Now create controllers/postsController.js:
import { createPost } from '../services/postsService.js';
export async function addPost(req, res, next) {
try {
const { title, order } = req.body;
const newPost = await createPost({ title, order });
return res.status(201).json(newPost);
} catch (error) {
return next(error);
}
}
A few things to notice:
- You extract
titleandorderfromreq.body, which Express populates thanks toexpress.json(). - You call a service function
createPostand await its result. - On success, you return the created post with status code
201 Created. - Errors are passed to the next middleware, which can be a shared error handler.
3. Implementing the service
Create services/postsService.js:
import db from '../db.js';
import { nanoid } from 'nanoid';
export async function createPost({ title, order }) {
await db.read();
const post = {
id: nanoid(),
title,
order,
};
db.data.posts.push(post);
await db.write();
return post;
}
Here you:
- Re-read the database file to make sure you have the latest data.
- Create a new
postobject with a uniqueidgenerated bynanoid. - Push the new post into the
postsarray. - Write the changes back to disk and return the created object.
In a more complex project, you might add validation, default values, or more advanced error handling inside this service layer.
4. Testing the endpoint with Postman
To confirm everything works, open Postman or a similar API testing tool.
- Set the HTTP method to
POST. - Use the URL
http://localhost:3000/posts. - In the body tab, choose
rawandJSON, then send something like:
{
"title": "Learn Node.js basics",
"order": 1
}
If your implementation is correct, you should receive a 201 Created status code along with the newly created todo object, including its generated id. You can check the db.json file to see that your data has been persisted.
Where to go next
You now have a working API endpoint that creates todos and stores them on disk. From here, common next steps include:
- Implementing
GET /poststo list all todos. - Adding
PATCHorPUTroutes to update existing items. - Implementing
DELETE /posts/:idto remove items. - Adding validation and error-handling middleware so that invalid requests receive clear error messages.
You might also replace LowDB with a full database such as PostgreSQL or MongoDB. The layered architecture you used here will make that transition smoother, because most of your changes will be isolated to the data persistence layer.
Conclusion
In this tutorial, you set up a Node.js environment, explored a simple but realistic project structure, and built a Todo List API using Express and LowDB. You saw how requests move through routers, controllers, and services before finally reaching the database.
The goal was not to cover every Node.js feature, but to give you a mental model for how backend applications are organised in 2026. Once you understand this flow, you can confidently explore advanced topics like authentication, security, testing, and deploying your apps to the cloud.
If you would like guided support, projects, and mentorship, consider joining Code Labs Academy's Full-Stack Web Development Bootcamp. It is a solid way to deepen your Node.js skills and become a job-ready full-stack developer.
Ready to upskill? Explore more learning paths with Code Labs Academy