Build a Task Manager REST API
Build a REST API with Node.js and Express that handles full CRUD operations.
Introduction
30 Second Summary
Every app you use sends requests to a server behind the scenes. When you scroll Instagram, search Spotify, or check your email, a backend server is doing the real work of fetching and organizing your data.
In this project, you will build a Task Manager API from scratch using Node.js and Express. You'll create a server that can create, read, update, and delete tasks, the same way a real backend powers a web application.
What You'll Build
A REST API server that runs locally on your computer, responds to HTTP requests, and manages task data using Express routing and middleware.
By the end of this project, you'll have:
- A working Express.js server with 5 API endpoints handling full CRUD operations.
- Hands-on experience sending HTTP requests with curl and understanding the request/response cycle.
- A project structure that demonstrates backend fundamentals ready to discuss in internship interviews.
- Secret Mission: Add filtering so users can request only completed or pending tasks.
Are there any prerequisites?
None. This project assumes no prior experience with backend development or JavaScript. Every piece of JavaScript syntax is explained as it appears, so you can follow along even if this is your first time writing code.
Before We Start
Before We Start...
Set Up Your Project
Every web application has a backend server that listens for requests and sends back data. In this step, you'll set up the foundation your Task Manager API will run on.
You'll create your project folder, install Express (the library that makes building servers in Node.js simple), and prepare the file where your code will live.
In this step, get ready to:
- Create your project folder and install Express.
- Create the server file where your backend code will live.
- Verify everything is ready for coding.
Create your project folder and initialize it
Before writing any code, you need a dedicated folder for your project and a way to track its dependencies. Node.js uses a file called package.json to keep track of which libraries your project depends on.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir task-api
cd task-api
- Confirm the folder exists by running:
ls ~/Desktop
You should see task-api in the output.
- Initialize your project by running this command:
npm init -y
What does this command do?
npm init -y creates a package.json file with default values. This file is your project's manifest. It tracks the name, version, and most importantly, the dependencies (libraries) your project needs.
You should see a confirmation in your terminal showing the contents of the new package.json file.
- Install Express by running this command:
npm install express@5.2.1
What does this command do?
npm install downloads the Express library and saves it to a node_modules folder in your project. The @5.2.1 part pins the exact version so everyone building this project uses the same code.
npm also adds Express to the dependencies section of your package.json automatically.
You should see output showing packages being added, ending with a line like added X packages.
Seeing a network error?
Make sure you have an internet connection. The npm install command downloads Express from the npm registry online.
If you see a permissions error, try running the command with sudo npm install express@5.2.1 and enter your password when prompted.
help me fix an npm install error
Create your server file
Your project needs a main file where all the server code will live. By convention, this file is called server.js.
- Create an empty server.js file by running this command:
touch server.js
- Confirm the file exists by running:
ls
You should see node_modules, package-lock.json, package.json, and server.js in the output.
- Open the task-api folder in your code editor.
Open from the terminal
If you use VS Code, you can run code . in your terminal to open the current folder directly. Other editors may have similar commands.
Now you need to tell Node.js that server.js is your project's entry point. By default, npm init sets the entry point to index.js. You'll update this to match your actual file.
- In your code editor, open package.json.
- Find this line:
"main": "index.js",
- Change index.js to server.js, so the line now looks like this:
"main": "server.js",
- Save package.json.
Why change the main field?
The main field in package.json tells Node.js which file is your project's entry point. Since your server code will live in server.js, this field should point there.
Verify your setup
Before moving on, confirm that everything is in place.
- In your code editor, open package.json and confirm that express appears under the dependencies section.
- Confirm the node_modules folder exists in your file explorer sidebar. This is where npm stored the Express library code.
✔️ Awesome, I've got everything!
Great. Double check you've saved package.json and you're ready to move on.
ⓧ I'd like to double check the full code
Your package.json should look exactly like this:
{
"name": "task-api",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "5.2.1"
}
}
Your server.js file should exist but be empty. You'll add code to it in the next step.
Your project is ready to go. Next up, you'll write your first server code and see it respond to real requests.
Create Your First Server
Your project folder is ready and Express is installed. Now it's time to write actual server code.
A server is a program that listens for incoming requests and sends back responses. Every app you use (Instagram, Spotify, Gmail) has a server like this running behind the scenes. In this step, you'll create one from scratch using Express.
In this step, get ready to:
- Write a basic Express server that responds to requests.
- Run the server and test it with a real HTTP request.
- Add middleware to handle JSON data from future requests.
Write the basic server code
Your server needs to do three things: load the Express library, define what happens when someone visits a URL, and start listening for requests. Let's write all of that now.
What are const and let?
JavaScript uses const to create a variable that can't be reassigned. Think of it as giving a permanent name to something. let is similar but allows you to change the value later.
We'll use const for everything in this step because none of these values need to change.
- In your code editor, open server.js and add the following code:
const express = require('express');
const app = express();
const port = 3000;
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Task Manager API!' });
});
// Start the server
app.listen(port, () => {
console.log(`Task Manager API running at http://localhost:${port}`);
});
What does this code do?
const express = require('express') loads the Express library you installed in Step 1. const app = express() creates your application instance. This is the object you'll attach all your routes to.
app.get('/', (req, res) => { ... }) defines a route. It says: when someone sends a GET request to the / URL, run this function. The function receives two inputs: req (the incoming request) and res (the response you send back).
app.listen(port, () => { ... }) starts the server on port 3000. The function inside runs once the server is ready, printing a confirmation message.
What is an arrow function?
(req, res) => { } is an arrow function. It's a shorthand way to write a function in JavaScript. The values in () are the inputs, and the code in {} is what the function does. It's identical to writing function(req, res) { } but shorter.
- Save server.js.
Seeing red squiggles in your editor?
Some editors flag require as an issue if they expect ES module syntax. This is safe to ignore. Your code uses CommonJS (the require style), which is fully supported by Node.js.
help me understand why my editor is showing errors on require
Run your server and test it
Your server code is written. Time to bring it to life and prove it works by sending it a real HTTP request.
- Start your server by running this command in your terminal:
node server.js
You should see this message printed in your terminal:
Task Manager API running at http://localhost:3000
What does this output mean?
This message comes from the console.log inside your app.listen callback. It confirms your server started successfully and is now waiting for requests on port 3000.
Your terminal is now occupied running the server. It won't accept new commands until you stop the server.
- Open a second terminal tab or window. On most terminals, press Cmd+T (macOS) or Ctrl+Shift+T (Linux) to open a new tab.
- Send a request to your server by running:
curl http://localhost:3000
You should see this JSON response:
{"message":"Welcome to the Task Manager API!"}
You just built a working web server and sent it your first request.
Seeing connection refused?
Make sure your server is still running in the first terminal. If you see the regular command prompt (no Task Manager API running at... message), the server stopped. Run node server.js again in that terminal.
Also confirm you're running the curl command in a different terminal tab or window, not the same one running the server.
help me debug why curl can't connect to my Express server on localhost:3000
Add JSON middleware
Your server can respond to requests, but it can't yet understand data sent TO it. When someone sends your API a new task (as JSON text), Express needs to convert that text into a JavaScript object your code can work with.
What is middleware?
Middleware is code that runs between receiving a request and sending a response. Think of it as a processing step. The express.json() middleware reads incoming JSON text and converts it into a JavaScript object you can access through req.body.
- Switch back to the terminal running your server. Press Ctrl+C to stop the server.
- In server.js, find this line:
const port = 3000;
- Add this code directly below it, leaving one blank line between:
// Middleware to parse JSON request bodies
app.use(express.json());
Why does this go before the routes?
Middleware runs in the order you define it. By placing app.use(express.json()) before your routes, every request passes through the JSON parser first. If you placed it after, your routes would see undefined for req.body because the parser never ran.
- Save server.js.
- Restart your server by running:
node server.js
- Switch to your second terminal and run curl http://localhost:3000 again. You should see the same welcome message as before.
Server won't start after editing?
Check that app.use(express.json()) is on its own line below const port = 3000; and above your app.get route. A missing semicolon or parenthesis will prevent the server from starting.
If you see a syntax error in the terminal, look at the line number it mentions and compare your code with the double-check tab below.
help me fix a syntax error in my Express server.js file
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and your server is running.
ⓧ I'd like to double check the full code
Here's what your complete server.js should look like at this point:
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Task Manager API!' });
});
// Start the server
app.listen(port, () => {
console.log(`Task Manager API running at http://localhost:${port}`);
});
Your server is alive and ready to receive data. Next up, you'll build the endpoints that actually create and list tasks.
Build the GET and POST Endpoints
Your server is alive and responding to requests. Now it's time to make it do something useful.
In this step, you'll teach your server how to store tasks and respond to two types of HTTP requests: GET (give me data) and POST (here's new data). These two operations are the backbone of every web app you've ever used.
In this step, get ready to:
- Create an in-memory data store to hold your tasks.
- Build a GET endpoint that returns all tasks as JSON.
- Build a POST endpoint that creates new tasks.
Create an in-memory data store and GET /tasks
Your API needs somewhere to keep tasks. A real production app would use a database, but for learning the request/response cycle, a JavaScript array works perfectly.
- In server.js, find the comment that says // Welcome route.
- Add the following code directly above that comment to create your data store and GET route:
// In-memory data store
let tasks = [
{ id: 1, title: 'Learn Node.js basics', completed: false }
];
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Task Manager API!' });
});
// GET /tasks - Get all tasks
app.get('/tasks', (req, res) => {
res.json(tasks);
});
What does this code do?
let tasks = [...] creates an array with one seed task already inside it. let is like const but allows you to change the value later. You need this because you'll be adding and removing tasks.
An array [] holds a list of items. An object {} holds key-value pairs. So [{ id: 1, title: 'Learn Node.js basics', completed: false }] is an array containing one object.
app.get('/tasks', ...) tells Express: when someone sends a GET request to /tasks, respond with the entire tasks array as JSON. res.json(tasks) converts the array to JSON and sends it back.
- Save server.js.
You won't see a change yet. The next chunk adds the POST route, then you'll restart and test both together.
Create POST /tasks to add new tasks
GET retrieves data. POST sends new data to the server. Your POST endpoint will accept a task title, create a new task object, and add it to the array.
- In server.js, find the GET /tasks route you just added.
- Add the following POST route directly below it:
// POST /tasks - Create a new task
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newTask = {
id: Date.now(),
title: title,
completed: false
};
tasks.push(newTask);
res.status(201).json(newTask);
});
What does this code do?
const { title } = req.body is called destructuring. It's a shortcut for const title = req.body.title. It pulls a property out of an object in one step.
The if (!title) check validates that the request included a title. If not, the server responds with a 400 status code (meaning "bad request") and stops.
Date.now() generates a unique number based on the current time. This gives each task a unique ID. tasks.push(newTask) adds the new task to the end of the array.
res.status(201).json(newTask) sends back the newly created task with a 201 status code. 201 means "created successfully".
- Save server.js.
You won't see a change yet. The next substep restarts the server and tests both routes.
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js before moving on.
ⓧ I'd like to double check the full code
Here's the full server.js as it should look now:
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// In-memory data store
let tasks = [
{ id: 1, title: 'Learn Node.js basics', completed: false }
];
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Task Manager API!' });
});
// GET /tasks - Get all tasks
app.get('/tasks', (req, res) => {
res.json(tasks);
});
// POST /tasks - Create a new task
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newTask = {
id: Date.now(),
title: title,
completed: false
};
tasks.push(newTask);
res.status(201).json(newTask);
});
// Start the server
app.listen(port, () => {
console.log(`Task Manager API running at http://localhost:${port}`);
});
Test both endpoints with curl
Your code is ready. Now you need to restart the server so it picks up the changes, then fire off some requests to see it in action.
- Switch back to the terminal where your server is running.
- Press Ctrl+C to stop the server.
- Restart the server by running:
node server.js
- You should see Task Manager API running at http://localhost:3000 in the terminal.
- Switch to your second terminal. Test the GET endpoint by running:
curl http://localhost:3000/tasks
- You should see the seed task you added to your data store:
[{"id":1,"title":"Learn Node.js basics","completed":false}]
- Now test creating a new task with a POST request by running:
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"Learn Express"}'
- You should see your newly created task returned with a unique ID and completed: false.
- Confirm the new task was saved by running GET again:
curl http://localhost:3000/tasks
- You should now see two tasks in the array: the original seed task and the new "Learn Express" task.
What just happened?
You just completed the "CR" in CRUD (Create, Read, Update, Delete). Your POST endpoint created a new task and stored it in memory. Your GET endpoint read the full list back.
The -X POST flag tells curl which HTTP method to use. The -H flag sets a header (telling the server the data is JSON). The -d flag sends the request body.
Not seeing the expected output?
- If you see Cannot GET /tasks, make sure you stopped the old server with Ctrl+C and restarted it with node server.js.
- If your POST returns an empty response or no task is created, check that you included the -H "Content-Type: application/json" header in your curl command.
- If you see a syntax error when starting the server, open server.js and check for missing commas, brackets, or parentheses around the code you added.
Help me debug my GET and POST endpoints
Your API can now create and read tasks. Next up, you'll add the ability to update and delete them, completing the full CRUD cycle that powers every web application.
Build the PUT and DELETE Endpoints
Your API can already create and read tasks. But what happens when a user finishes a task and wants to mark it complete? Or when they want to remove a task entirely?
In this step, you'll add the remaining two operations: updating and deleting. Together with GET and POST, these form the full CRUD pattern (Create, Read, Update, Delete) that powers almost every web application's backend.
In this step, get ready to:
- Create a PUT endpoint to update existing tasks.
- Create a DELETE endpoint to remove tasks.
- Test the full CRUD cycle with curl.
Create PUT /tasks/:id to update a task
When a user completes a task, they need a way to update its status. The PUT method is the standard HTTP verb for replacing or updating a resource.
The tricky part is identifying which task to update. You'll use a route parameter (:id) in the URL. Express captures whatever value sits in that position and makes it available in req.params.id.
- In server.js, find the comment that says // Start the server.
- Add the following PUT route above that comment:
// PUT /tasks/:id - Update a task
app.put('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
if (req.body.title !== undefined) {
task.title = req.body.title;
}
if (req.body.completed !== undefined) {
task.completed = req.body.completed;
}
res.json(task);
});
What does this code do?
This route handles requests to URLs like /tasks/1 or /tasks/42. Here's how each piece works:
- Number(req.params.id) converts the URL parameter from a string to a number. Route parameters always arrive as strings, but your task IDs are numbers.
- .find(t => t.id === id) loops through the tasks array and returns the first task where the condition is true. t is a temporary variable name for each task as it loops.
- The !== undefined checks let users update just the title, just the completed status, or both. Only fields the user sends get changed.
- If no matching task exists, the route responds with a 404 status code meaning "not found".
- Save server.js.
You won't see a change yet. The next chunk adds the DELETE route, then you'll restart the server and test both together.
Seeing a syntax error when you save?
Make sure the closing }); of the PUT route is on its own line. A missing parenthesis or brace is the most common cause of syntax errors in route handlers.
Also check that this route sits after the POST route and before the // Start the server comment.
help me fix a syntax error in my Express PUT route
Create DELETE /tasks/:id to remove a task
Deleting follows a similar pattern to updating. You find the task by ID, then remove it from the array. The difference is you'll use .findIndex() instead of .find(). It works the same way but returns the position number instead of the item itself.
- In server.js, find the // Start the server comment again.
- Add the following DELETE route above it (below the PUT route you just added):
// DELETE /tasks/:id - Delete a task
app.delete('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const index = tasks.findIndex(t => t.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Task not found' });
}
tasks.splice(index, 1);
res.json({ message: 'Task deleted successfully' });
});
What does this code do?
- .findIndex(t => t.id === id) loops through the array and returns the position (index) of the matching task. If no match is found, it returns -1.
- tasks.splice(index, 1) means "at this position, remove 1 item". It modifies the array in place.
- After removing the task, the route sends back a confirmation message so the client knows the deletion worked.
- Save server.js.
Not sure where to place the DELETE route?
Your routes in server.js should be in this order from top to bottom: welcome GET /, GET /tasks, POST /tasks, PUT /tasks/:id, DELETE /tasks/:id, then app.listen.
help me figure out where to put my DELETE route in server.js
Test the full CRUD cycle with curl
Time to restart your server and run through the complete Create, Read, Update, Delete cycle. This proves all four operations work together.
- Switch to the terminal where your server is running.
- Press Ctrl+C to stop the server.
- Restart the server by running:
node server.js
You should see Task Manager API running at http://localhost:3000 printed in the terminal.
- Open a second terminal tab or window for testing.
- Create a new task by running:
curl -X POST http://localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"Test CRUD cycle"}'
You should see a response with your new task, including its generated id. Copy that id number for the next commands.
- Read all tasks to confirm it was created by running:
curl http://localhost:3000/tasks
You should see both the seed task and your new task in the array.
- Update the task's status to completed by running this command (replace 1 with the actual id from the POST response):
curl -X PUT http://localhost:3000/tasks/1 -H "Content-Type: application/json" -d '{"completed":true}'
You should see the task returned with "completed":true.
- Delete the task by running (again replacing 1 with the actual id):
curl -X DELETE http://localhost:3000/tasks/1
You should see {"message":"Task deleted successfully"} confirming the removal.
- Run one final GET to confirm the task is gone:
curl http://localhost:3000/tasks
What is CRUD?
CRUD stands for Create, Read, Update, Delete. These four operations are the foundation of almost every application. Instagram posts (create, read, edit, delete), Spotify playlists (same pattern), bank transactions (same pattern). You just implemented the entire pattern from scratch.
Getting "Task not found" on PUT or DELETE?
The id in your URL must match an existing task's id exactly. Run curl http://localhost:3000/tasks to see the current tasks and their IDs.
Remember that your in-memory data resets every time you restart the server. If you restarted between commands, only the seed task (id 1) exists.
help me debug why my PUT or DELETE request returns Task not found
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and your server is running without errors.
ⓧ I'd like to double check the full code
const express = require('express');
const app = express();
const port = 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// In-memory data store
let tasks = [
{ id: 1, title: 'Learn Node.js basics', completed: false }
];
// Welcome route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the Task Manager API!' });
});
// GET /tasks - Get all tasks
app.get('/tasks', (req, res) => {
res.json(tasks);
});
// POST /tasks - Create a new task
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newTask = {
id: Date.now(),
title: title,
completed: false
};
tasks.push(newTask);
res.status(201).json(newTask);
});
// PUT /tasks/:id - Update a task
app.put('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
if (req.body.title !== undefined) {
task.title = req.body.title;
}
if (req.body.completed !== undefined) {
task.completed = req.body.completed;
}
res.json(task);
});
// DELETE /tasks/:id - Delete a task
app.delete('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const index = tasks.findIndex(t => t.id === id);
if (index === -1) {
return res.status(404).json({ error: 'Task not found' });
}
tasks.splice(index, 1);
res.json({ message: 'Task deleted successfully' });
});
// Start the server
app.listen(port, () => {
console.log(`Task Manager API running at http://localhost:${port}`);
});
Your API now handles the full CRUD cycle. Next up, you'll put it all together in the Secret Mission and add filtering so users can request only the tasks they care about.
Secret mission
Add Task Filtering
Your API returns every task every time. But real users want to see only what's relevant. What if they could ask for just completed tasks, or just pending ones? In this mission, you'll add query parameter filtering to your GET endpoint.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files for reference, or delete them to free up disk space.
Resources you used:
- The running Node.js server process (port 3000).
- The task-api project folder (includes server.js, package.json, and node_modules/).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep your project files for reference.
- Your server will keep running until you press Ctrl+C in the terminal or close the terminal window.
- Your project files stay in the task-api folder on your Desktop.
Pause - I'll come back to this later
Stop the running server to free up the port, but keep your files so you can pick up where you left off.
- Switch to the terminal where your server is running and press Ctrl+C to stop the process.
- Your task-api folder and all code remain on disk. To restart later, navigate back into the folder and run node server.js.
Delete - I don't want to use this again
Remove all project files and stop the server. This frees up disk space and the port.
- Switch to the terminal where your server is running and press Ctrl+C to stop the process.
- Delete the project folder by running this command from your Desktop:
cd ~/Desktop
rm -rf task-api
- On Windows, use this command instead:
cd ~\Desktop
rmdir /s /q task-api
- Confirm the folder is gone by running ls (or dir on Windows). You should no longer see task-api in the output.
Nice Work!
Nice Work!
You just built a REST API from scratch. This is the exact same pattern that powers the backend of every web application you use daily.
Here's what you learned:
- Built a complete Express server with full CRUD operations (Create, Read, Update, Delete) that handles tasks through five API endpoints.
- Used curl to send HTTP requests and understood the request/response cycle that every frontend relies on to communicate with a backend.
- Practiced core JavaScript patterns including arrow functions, destructuring, and array methods like .find(), .filter(), and .splice() in a real backend context.
- Secret Mission: Added query parameter filtering so users can request only completed or pending tasks.
Ready to quiz yourself?