Build a Task Manager REST API

Build a REST API with Node.js and Express to manage tasks via HTTP.

Introduction

30 Second Summary

Every time you check the weather on your phone, scroll social media, or add something to an online shopping cart, your device is talking to a server behind the scenes. That server receives your request, processes it, and sends back exactly the data you need.

In this project, you will build a Task Manager REST API using Node.js and Express. Your server will respond to HTTP requests with structured JSON data, showing you exactly how real-world backends work.

What You'll Build

A REST API that lets you create, read, update, and delete tasks through HTTP requests sent from your terminal.

By the end of this project, you'll have:

  • An Express server that listens for HTTP requests and returns JSON responses on port 3000.
  • Five RESTful endpoints covering all CRUD operations: list tasks, get one task, create a task, update a task, and delete a task.
  • Input validation that rejects bad data with helpful error messages.
  • Secret Mission: Add filtering and sorting so users can fetch only completed tasks or sort results by newest first.

Are there any prerequisites?

You only need basic familiarity with JavaScript (variables, functions, arrays, and objects). No prior backend experience is needed.

Before We Start

Before you can build a REST API, you need the right tools on your machine. Node.js is the JavaScript runtime that will power your server, and a few packages will give you a head start on building endpoints.

In this step, you will install Node.js, create your project folder, and configure it so you are ready to write server code.

In this step, get ready to:
  • Install Node.js and confirm it works in PowerShell.
  • Create your project folder and install packages.
  • Configure your project for modern JavaScript.
Install Node.js

Node.js lets you run JavaScript outside the browser. Your API server will run on Node.js, and it requires version 18 or higher.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Check if Node.js is already installed by running this command:
node --version

Look at the output and pick the tab below that matches what you see.

✔️ I see version 18 or higher

Node.js is ready. Confirm that npm (the Node package manager) also works by running:

npm --version

If you see a version number, you are all set. Move on to the next substep.

ⓧ I see a version lower than 18

You need Node.js 18 or higher for this project. The Windows installer will replace your current version automatically.

  • Go to https://nodejs.org in your browser.
  • Download the LTS installer (the large green button on the left).
  • Run the installer and accept all the defaults.
  • Close and reopen PowerShell after the install finishes.
  • Run node --version again to confirm you see version 18 or higher.

ⓧ Command not found

Node.js is not installed yet.

  • Go to https://nodejs.org in your browser.
  • Download the LTS installer (the large green button on the left).
  • Run the installer and accept all the defaults.
  • Close and reopen PowerShell after the install finishes.
  • Run node --version to confirm Node.js is installed.
  • Run npm --version to confirm npm is available too.
Create the project folder and install packages

Every Node.js project lives in its own folder with a package.json file that tracks dependencies and scripts.

  • 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
  • Initialize a new Node.js project by running:
npm init -y

What does this command do?

The -y flag tells npm to accept all the default settings. This creates a package.json file instantly without asking you questions.

  • Confirm the file was created by running:
ls

You should see package.json in the output.

  • Install Express (the web framework for your API) and nodemon (a dev tool that auto-restarts your server) by running these commands:
npm install express
npm install --save-dev nodemon

What does this command do?

The first command installs Express as a regular dependency your project needs to run. The second command installs nodemon as a dev dependency, meaning it is only used during development to auto-restart your server whenever you save a file.

You should see a success message for each install and a new node_modules folder appear in your project.

Seeing permission errors?

If npm fails with a permissions error, try closing PowerShell and reopening it with Run as administrator (right-click the PowerShell icon and select it).

help me fix npm permission errors on Windows

Configure your package.json

The default package.json needs a few changes before you can write modern JavaScript and use nodemon for development.

  • Open your project in VS Code by running:
code .

Command not recognized?

If code . does not work, close and reopen PowerShell to refresh your PATH. If it still fails, open VS Code manually (press the Windows key, type Visual Studio Code, press Enter), then click File > Open Folder and select the task-api folder on your Desktop.

help me open my project folder in VS Code on Windows

  • In VS Code, click on package.json in the Explorer sidebar on the left to open it.
  • Select all the text in the file (Ctrl+A) and replace it with this code:
{
  "name": "task-api",
  "version": "1.0.0",
  "description": "A simple task manager REST API",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^5.2.1"
  },
  "devDependencies": {
    "nodemon": "^3.1.14"
  }
}

What did you change?

Here are the key differences from the default file:

  • "type": "module" enables modern import/export syntax instead of the older require() style.
  • "main": "server.js" tells Node.js which file is the entry point for your app.
  • "dev": "nodemon server.js" creates a shortcut so you can start your server with auto-restart by running npm run dev.
  • Save the file by pressing Ctrl+S.

Version numbers don't match exactly?

Your dependencies section might show slightly different version numbers depending on when you installed. That is completely fine. The important parts are the type, main, and scripts fields.

✔️ Awesome, I've got everything!

Great. Make sure you have saved package.json before moving on.

ⓧ I'd like to double check the full code

Here is the complete package.json you should have:

{
  "name": "task-api",
  "version": "1.0.0",
  "description": "A simple task manager REST API",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^5.2.1"
  },
  "devDependencies": {
    "nodemon": "^3.1.14"
  }
}

Your development environment is ready. Next up, you will create the Express server and see it respond to its first request.

Create Your Express Server

Your development environment is ready with Node.js, Express, and nodemon installed. Now it's time to write the actual server code that handles HTTP requests.

Every REST API needs a server that listens for incoming requests and sends back responses. In this step, you will create an Express server, give it a health-check route, and make sure unknown URLs get a proper error response.

In this step, get ready to:
  • Create server.js with Express configured to parse JSON and a health-check route.
  • Start the dev server and verify it responds in your browser.
  • Add a 404 catch-all handler for unmatched routes.
Create server.js with Express setup

Your server file is where Express receives HTTP requests and decides what to send back. You will set up the app, enable JSON parsing, and define your first route.

  • In VS Code's Explorer sidebar, click the New File icon next to your task-api folder name.
  • Name the file server.js and press Enter to create it.
  • Add the server setup code by pasting the following into server.js:
import express from 'express';

const app = express();
const PORT = 3000;

app.use(express.json());

What does this code do?

  • import express from 'express' loads the Express library using modern JavaScript module syntax.
  • const app = express() creates your application instance. This object is what you attach routes and middleware to.
  • const PORT = 3000 stores the port number your server will listen on.
  • app.use(express.json()) tells Express to automatically parse incoming JSON request bodies so you can access them in your route handlers.
  • Below the setup code, add the root route and server startup by pasting this code:
app.get('/', (req, res) => {
  res.json({ message: 'Task Manager API is running' });
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

What does this code do?

  • app.get('/', ...) defines a route that responds to GET requests at the root URL (/). This is your health-check endpoint. When someone visits your server's base URL, they get a JSON message confirming the API is running.
  • app.listen(PORT, ...) starts the server and tells it to listen for requests on port 3000. The callback function runs once the server is ready and logs a confirmation message.
  • Save server.js by pressing Ctrl+S.
Start the server and verify it works

With your server file saved, you can start Express and confirm it responds to requests.

  • Start the development server by running this command in your terminal:
npm run dev

You should see output similar to this in your terminal:

[nodemon] starting `node server.js`
Server running at http://localhost:3000

What is nodemon doing?

Nodemon watches your project files for changes. Every time you save a file, it automatically restarts the server so you see your latest code in action without manually stopping and restarting.

  • Open your browser and navigate to http://localhost:3000.
  • You should see a JSON response: {"message":"Task Manager API is running"}.

Don't see the JSON response?

  • Check your terminal for error messages. A typo in server.js will prevent nodemon from starting the server.
  • Make sure you saved server.js before running npm run dev.
  • Confirm the URL is exactly http://localhost:3000 with no trailing path.

Help me debug why my Express server is not responding at localhost:3000.

Add a 404 catch-all handler

Right now, if someone requests a URL that does not exist on your server, Express returns a default HTML error page. A proper API should return a JSON error with a 404 status code instead.

  • In server.js, find this line:
app.listen(PORT, () => {
  • Add the following 404 catch-all handler directly above app.listen:
app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

Why does placement matter?

Express processes middleware and routes in the order you define them. This catch-all uses app.use() without a specific path, so it matches any request that did not hit an earlier route.

Placing it after your defined routes but before app.listen() means it only catches genuinely unmatched requests.

  • Save server.js. Nodemon will automatically restart your server.
  • Test the 404 handler by running this command in a new terminal:
curl.exe http://localhost:3000/nonexistent
  • You should see: {"error":"Route not found"}.

Still seeing an HTML error page?

  • Make sure the app.use((req, res) => {...}) block is placed above app.listen() but below your app.get('/', ...) route.
  • Check that nodemon restarted after you saved. Look for the [nodemon] restarting message in your original terminal.

Help me fix my 404 catch-all handler in Express.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js and that your server is still running.

ⓧ I'd like to double check the full code

Here is the complete server.js file:

import express from 'express';

const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Task Manager API is running' });
});

app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

Your Express server is up and running with a health-check route and proper error handling. Next up, you will build the task routes that let you create, read, update, and delete tasks.

Build Your Task Routes

Your server is running and responding to requests. Right now it only has a single health-check route and a catch-all 404 handler.

In this step, you'll create a dedicated routes file that handles all task-related requests. Separating routes into their own file keeps your code organized as your API grows.

In this step, get ready to:
  • Create a routes file with seed data for your tasks.
  • Implement GET endpoints to list all tasks and fetch one by ID.
  • Wire the routes into your server and test them with curl.
Create your task routes file

Instead of putting all your route handlers inside server.js, you'll organize them in a separate file. Express provides a Router object that lets you group related routes together and export them as a module.

  • In the VS Code Explorer sidebar, right-click inside your task-api folder and select New Folder.
  • Name the folder routes.
  • Right-click the new routes folder and select New File.
  • Name the file tasks.js.
  • Add the Router setup and seed data to routes/tasks.js by pasting this code:
import { Router } from 'express';

const router = Router();

// Starter tasks so the API has data to return immediately
let tasks = [
  { id: 1, title: 'Learn REST APIs', completed: false },
  { id: 2, title: 'Build an Express server', completed: false },
];
let nextId = 3;

What does this code do?

Router() creates a mini-app that you can attach route handlers to, then plug into your main server later.

The tasks array holds starter data in memory. The nextId counter tracks what ID to assign the next task you create. Data resets when the server restarts, which is fine for learning.

  • Save routes/tasks.js.
Add the GET endpoints

Your route file needs handlers that respond to incoming requests. A GET request asks the server for data without changing anything.

  • In routes/tasks.js, add the following code below the let nextId = 3; line:
// GET /api/tasks - List all tasks
router.get('/', (req, res) => {
  res.json({ tasks, total: tasks.length });
});

// GET /api/tasks/:id - Get a single task
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  res.json({ task });
});

export default router;

What does this code do?

  • router.get('/') handles GET requests to the base path. It returns all tasks as JSON along with a total count.
  • router.get('/:id') handles GET requests for a specific task. The :id is a route parameter that Express extracts from the URL into req.params.id.
  • parseInt() converts the string parameter to a number so it matches the numeric IDs in the array.
  • If no task matches, the handler returns a 404 status with an error message. This tells the client the resource does not exist.
  • export default router makes the router available for other files to import.
  • Save routes/tasks.js.

Seeing a syntax error in the terminal?

  • Check that every opening parenthesis and curly brace has a matching closing one. The router.get calls end with }); (closing the callback function and the .get() call).
  • Make sure there is no leftover text above the first import line.

Ask for help: Why am I getting a syntax error in my routes/tasks.js file?

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import { Router } from 'express';

const router = Router();

let tasks = [
  { id: 1, title: 'Learn REST APIs', completed: false },
  { id: 2, title: 'Build an Express server', completed: false },
];
let nextId = 3;

// GET /api/tasks - List all tasks
router.get('/', (req, res) => {
  res.json({ tasks, total: tasks.length });
});

// GET /api/tasks/:id - Get a single task
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  res.json({ task });
});

export default router;
Wire the router into your server and test

Your routes file exists, but your server does not know about it yet. You need to import the router and mount it at a URL path.

  • In server.js, find this line at the top:
import express from 'express';
  • Add this import directly below it:
import taskRoutes from './routes/tasks.js';
  • Now find the health-check route (the app.get('/', ...) block). Add this line below it, before the 404 catch-all:
app.use('/api/tasks', taskRoutes);

What does app.use do here?

app.use('/api/tasks', taskRoutes) tells Express to forward any request starting with /api/tasks to your router. Inside the router, '/' becomes /api/tasks and '/:id' becomes /api/tasks/:id.

Order matters. The 404 catch-all must stay at the bottom because Express checks routes from top to bottom. If the catch-all came first, it would intercept everything.

  • Save server.js. Nodemon restarts the server automatically.

Server crashed after saving?

  • Make sure the import path is exactly './routes/tasks.js' with the .js extension. ESM imports require the full file extension.
  • Confirm that app.use('/api/tasks', taskRoutes) appears before the 404 catch-all, not after it.

Ask for help: My server crashes when I add the taskRoutes import. What is wrong?

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import express from 'express';
import taskRoutes from './routes/tasks.js';

const app = express();
const PORT = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Task Manager API is running' });
});

app.use('/api/tasks', taskRoutes);

app.use((req, res) => {
  res.status(404).json({ error: 'Route not found' });
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

Now test your endpoints. You need a second terminal because your dev server is using the first one.

  • Open a new terminal in VS Code by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows).
  • List all tasks by running this command:
curl.exe http://localhost:3000/api/tasks

You should see a JSON response containing both starter tasks and a total of 2.

  • Fetch a single task by its ID by running:
curl.exe http://localhost:3000/api/tasks/1

You should see only the task with ID 1 returned.

  • Test the 404 handling by requesting a task that does not exist:
curl.exe http://localhost:3000/api/tasks/99

You should see {"error":"Task not found"} with no crash. Your API gracefully handles missing data.

Why curl.exe instead of curl?

PowerShell has a built-in alias that maps curl to a different command (Invoke-WebRequest). Using curl.exe calls the real curl program that ships with Windows 10 and above.

Getting an error from curl?

  • Make sure your dev server is still running in the other terminal tab. You should see the Server running at http://localhost:3000 message there.
  • Double check the URL starts with http:// not https://.

Ask for help: curl is not returning the expected JSON from my API. How do I debug this?

Your API can now serve task data to any client that asks for it. Next up, you'll add the ability to create, update, and delete tasks.

Add Create, Update, and Delete

Your API can retrieve tasks, but users cannot actually manage their to-do list yet.

In this step, you will add three new endpoints that let users create, update, and delete tasks. These three operations complete the CRUD pattern (Create, Read, Update, Delete) that nearly every REST API follows.

In this step, get ready to:
  • Add a POST endpoint that creates new tasks with input validation.
  • Add a PATCH endpoint that updates a task's title or completion status.
  • Add a DELETE endpoint that removes a task and returns the correct status code.
Add the POST endpoint for creating tasks

When a user sends data to your API, you cannot blindly trust it. The POST endpoint needs to validate that the request includes a proper title before creating a task.

  • In routes/tasks.js, find the line that says export default router; at the bottom of the file.
  • Add the following POST handler above that line:
// POST /api/tasks - Create a new task
router.post('/', (req, res) => {
  const { title } = req.body;

  if (!title || typeof title !== 'string' || title.trim().length === 0) {
    return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
  }

  const task = {
    id: nextId++,
    title: title.trim(),
    completed: false,
  };

  tasks.push(task);
  res.status(201).json({ task });
});

What does this code do?

  • const { title } = req.body pulls the title field from the JSON body the client sent.
  • The if block rejects requests where the title is missing, not a string, or only whitespace. It returns a 400 status code meaning "bad request".
  • nextId++ assigns a unique ID to each new task and increments the counter for the next one.
  • res.status(201).json({ task }) responds with status 201 ("Created") and sends back the new task as JSON.
  • Save routes/tasks.js.
  • Test the new endpoint by running this command in a new terminal tab:
curl.exe -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title": "My new task"}'

You should see a JSON response containing your new task with id: 3 and a 201 status.

Seeing an empty response or error?

  • Make sure you saved the file. Nodemon only restarts when it detects a change on disk.
  • Check that the -H "Content-Type: application/json" header is included. Without it, Express cannot parse the JSON body.
  • Confirm the JSON in -d uses double quotes around keys and values inside the single-quoted string.

Why is my POST endpoint returning an error or empty response?

Add the PATCH endpoint for updating tasks

Users need to mark tasks as done or fix a typo in a title. A PATCH request updates only the fields the client sends, leaving everything else unchanged.

  • In routes/tasks.js, add the following PATCH handler below the POST handler you just wrote and above export default router;:
// PATCH /api/tasks/:id - Update a task
router.patch('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  const { title, completed } = req.body;

  if (title !== undefined) {
    if (typeof title !== 'string' || title.trim().length === 0) {
      return res.status(400).json({ error: 'Title must be a non-empty string' });
    }
    task.title = title.trim();
  }

  if (completed !== undefined) {
    task.completed = Boolean(completed);
  }

  res.json({ task });
});

What does this code do?

  • The handler finds the task by its ID from the URL, just like the GET /:id route.
  • It only updates fields that the client actually sent. If title is not in the request body, it stays unchanged.
  • Boolean(completed) converts whatever the client sends into a true/false value, keeping data types consistent.
  • The updated task is sent back so the client can confirm the changes took effect.
  • Save routes/tasks.js.
  • Test the PATCH endpoint by marking task 1 as completed:
curl.exe -X PATCH http://localhost:3000/api/tasks/1 -H "Content-Type: application/json" -d '{"completed": true}'

You should see task 1 returned with "completed": true in the response.

Task not updating?

  • Make sure the URL ends with /1 (or another valid task ID). A missing ID means the request hits the wrong route.
  • Check that the PATCH handler is placed after the POST handler and before export default router;.

Why is my PATCH request not updating the task?

Add the DELETE endpoint for removing tasks

Deleting a resource is different from other operations because a successful delete has nothing to send back. The standard response is status code 204, which means "No Content" and tells the client the deletion succeeded.

  • In routes/tasks.js, add the following DELETE handler below the PATCH handler and above export default router;:
// DELETE /api/tasks/:id - Delete a task
router.delete('/:id', (req, res) => {
  const id = parseInt(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.status(204).end();
});

What does this code do?

  • tasks.findIndex() returns the position of the task in the array, or -1 if it does not exist.
  • tasks.splice(index, 1) removes exactly one item at that position from the array.
  • res.status(204).end() sends a 204 response with no body. The .end() method closes the response without writing any data.
  • Save routes/tasks.js.
  • Test the DELETE endpoint by removing task 2:
curl.exe -X DELETE http://localhost:3000/api/tasks/2

You should see no response body returned, which confirms the 204 No Content status. Verify the task is gone by listing all tasks:

curl.exe http://localhost:3000/api/tasks

The response should show only the remaining tasks. Task 2 is no longer in the list.

Still seeing the deleted task?

  • Make sure you saved the file after adding the DELETE handler. Nodemon needs to restart for changes to take effect.
  • Check that the DELETE handler uses tasks.splice(index, 1) and not tasks.find(). Finding a task does not remove it from the array.

Why is my DELETE endpoint not removing the task from the array?

✔️ Awesome, I've got everything!

Great. Double check you have saved routes/tasks.js before moving on.

ⓧ I'd like to double check the full code

import { Router } from 'express';

const router = Router();

let tasks = [
  { id: 1, title: 'Learn REST APIs', completed: false },
  { id: 2, title: 'Build an Express server', completed: false },
];
let nextId = 3;

// GET /api/tasks - List all tasks
router.get('/', (req, res) => {
  res.json({ tasks, total: tasks.length });
});

// GET /api/tasks/:id - Get a single task
router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  res.json({ task });
});

// POST /api/tasks - Create a new task
router.post('/', (req, res) => {
  const { title } = req.body;

  if (!title || typeof title !== 'string' || title.trim().length === 0) {
    return res.status(400).json({ error: 'Title is required and must be a non-empty string' });
  }

  const task = {
    id: nextId++,
    title: title.trim(),
    completed: false,
  };

  tasks.push(task);
  res.status(201).json({ task });
});

// PATCH /api/tasks/:id - Update a task
router.patch('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const task = tasks.find(t => t.id === id);

  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }

  const { title, completed } = req.body;

  if (title !== undefined) {
    if (typeof title !== 'string' || title.trim().length === 0) {
      return res.status(400).json({ error: 'Title must be a non-empty string' });
    }
    task.title = title.trim();
  }

  if (completed !== undefined) {
    task.completed = Boolean(completed);
  }

  res.json({ task });
});

// DELETE /api/tasks/:id - Delete a task
router.delete('/:id', (req, res) => {
  const id = parseInt(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.status(204).end();
});

export default router;

Your API now supports all four CRUD operations. Next up, you will put every endpoint through its paces with a full round of testing.

Test Your Complete API

Your API has all five endpoints wired up and ready to go. Now it is time to put them through their paces by sending real HTTP requests and inspecting the responses.

Testing with curl.exe in the terminal is how professional developers verify API behavior before building a frontend. You will send requests, read the JSON that comes back, and confirm your server handles both happy paths and error cases correctly.

In this step, get ready to:
  • Create several tasks and list them all.
  • Update a task and verify the change.
  • Delete a task and test error cases.
Open a second terminal for testing

Your server is still running in the first terminal. You need a separate terminal to send requests to it.

  • In VS Code, click the + icon in the top-right corner of the terminal panel to open a new terminal tab.

Why a second terminal?

The first terminal is occupied running your server process. The second terminal is where you will type curl.exe commands to talk to that server.

Create tasks and list them all

Your API already has two seed tasks (from the array in routes/tasks.js). You will add two more using POST requests, then list everything with a GET request.

  • Create your first new task by running this command:
curl.exe -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title": "Buy groceries"}'

What does this command do?

  • curl.exe sends an HTTP request from your terminal. You use curl.exe (with the .exe) to bypass a PowerShell alias that would otherwise interfere.
  • -X POST tells curl to use the POST method, which creates a new resource.
  • -H "Content-Type: application/json" sets a header telling the server the body contains JSON.
  • -d sends the JSON body with the title of your new task.
  • You should see a response like this:
{"task":{"id":3,"title":"Buy groceries","completed":false}}

The server assigned ID 3 and set completed to false by default.

  • Create a second task by running:
curl.exe -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title": "Write documentation"}'
  • You should see a response with ID 4:
{"task":{"id":4,"title":"Write documentation","completed":false}}
  • List all tasks by running this GET request:
curl.exe http://localhost:3000/api/tasks
  • You should see all four tasks (the two seed tasks plus the two you just created):
{"tasks":[{"id":1,"title":"Learn REST APIs","completed":false},{"id":2,"title":"Build an Express server","completed":false},{"id":3,"title":"Buy groceries","completed":false},{"id":4,"title":"Write documentation","completed":false}],"total":4}

Seeing an error about connecting?

Make sure your server is still running in the other terminal tab. If you accidentally closed it, switch back to the first terminal and run npm run dev again.

If you see garbled output or an HTML error, double-check you typed curl.exe (not just curl). PowerShell without the .exe routes to a different command.

help me debug why curl.exe is not connecting to my server

Update a task and verify the change

PATCH requests let you update specific fields on an existing resource without replacing the whole thing. You will mark the "Buy groceries" task as completed, then verify the change stuck.

  • Mark task 3 as completed by running this PATCH request:
curl.exe -X PATCH http://localhost:3000/api/tasks/3 -H "Content-Type: application/json" -d '{"completed": true}'
  • You should see the updated task with completed now set to true:
{"task":{"id":3,"title":"Buy groceries","completed":true}}
  • Verify the change persisted by fetching that specific task:
curl.exe http://localhost:3000/api/tasks/3
  • You should see the same result confirming completed is still true.

Why verify with a separate GET?

The PATCH response shows what the server returned at the moment of the update. A follow-up GET proves the change was actually saved in memory and not just echoed back.

Delete a task and test error cases

A reliable API needs to handle bad input gracefully. You will delete a task, confirm it is gone, then deliberately send invalid requests to see your validation in action.

  • Delete task 2 by running:
curl.exe -X DELETE http://localhost:3000/api/tasks/2

Why is there no response body?

A 204 No Content status means the deletion succeeded but the server intentionally sends back an empty body. This is the standard REST convention for successful deletes.

  • Confirm the task is gone by listing all tasks:
curl.exe http://localhost:3000/api/tasks
  • You should see only three tasks now (task 2 is missing from the list):
{"tasks":[{"id":1,"title":"Learn REST APIs","completed":false},{"id":3,"title":"Buy groceries","completed":true},{"id":4,"title":"Write documentation","completed":false}],"total":3}

Now test how your API responds to invalid requests.

  • Request a task that does not exist by running:
curl.exe http://localhost:3000/api/tasks/999
  • You should see a 404 error response:
{"error":"Task not found"}
  • Try creating a task without a title by running:
curl.exe -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{}'
  • You should see a 400 error with a helpful message:
{"error":"Title is required and must be a non-empty string"}

Why do error responses matter?

When a frontend app or another service calls your API, clear error messages help developers debug problems quickly. A generic "Internal Server Error" tells the caller nothing, but "Title is required and must be a non-empty string" tells them exactly what to fix.

Getting unexpected responses?

If task IDs do not match what you expect, your server may have restarted (which resets the in-memory data). Run the POST commands again to re-create your tasks before continuing.

If the JSON body is not being parsed (you see the "Title is required" error even when you include a title), check that your -H "Content-Type: application/json" header is present and the quotes around your JSON are correct.

help me figure out why my curl.exe commands are not sending JSON correctly

Secret mission

Add Filtering and Sorting

Your API returns all tasks every time. What if a user only wants completed tasks, or wants the newest tasks first? Add query parameter support to make your API smarter.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with no cloud resources or paid services. There are no ongoing costs to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The running nodemon dev server (in your terminal).
  • The task-api project folder on your Desktop.
  • Node.js (installed on your system).

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment more with your API.

  • Your server only runs while your terminal session is open. Closing the terminal stops it automatically.
  • Local files on your Desktop do not cost anything to keep.
  • Node.js stays installed on your system for future JavaScript projects.

Pause - I'll come back to this later

Stop the running server to free up your terminal. Your project files stay on your Desktop, ready to pick up again later.

  • In the terminal where your server is running, press Ctrl+C to stop nodemon.
  • When you are ready to resume, open a terminal, navigate to your task-api folder, and run npm run dev to start the server again.

Delete - I don't want to use this again

Remove all project files and optionally uninstall Node.js from your system.

  • In the terminal where your server is running, press Ctrl+C to stop nodemon.
  • Delete the project folder. On macOS/Linux, run:
cd ~/Desktop
rm -rf task-api

On Windows, run this in PowerShell:

cd ~/Desktop
Remove-Item -Recurse -Force task-api
  • To uninstall Node.js on Windows, open Settings, then Apps, then Installed apps. Find Node.js in the list and click Uninstall.
  • To uninstall Node.js on macOS (if installed via Homebrew), run:
brew uninstall node

Keep Node.js if you plan to learn more JavaScript

Node.js is useful for many types of projects beyond this one. If you plan to do more JavaScript or web development, you will likely want it installed.

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully functional REST API from scratch using Node.js and Express.

You've learned how to:

  • Built an Express server that handles HTTP requests and returns structured JSON responses.
  • Created five RESTful endpoints covering all CRUD operations (Create, Read, Update, Delete).
  • Implemented input validation that protects your API from bad data and returns helpful error messages.
  • Added filtering and sorting with query parameters so users can customize their results.

Ready to quiz yourself?