Build a To-Do API with Job Queues
Build a REST API with async background job processing using BullMQ and Redis.
Introduction
30 Second Summary
When you order food online, you don't stare at a loading screen while the restaurant gets notified. The app responds to you instantly and handles the notification separately, in the background.
In this project, you will build a To-Do API that offloads notification jobs to a background task queue, then expose it to the internet so anyone can interact with it.
What You'll Build
A REST API built with Fastify that stores to-do items in memory and dispatches background notification jobs through a BullMQ task queue backed by Redis.
By the end of this project, you'll have:
- A Fastify REST API with versioned endpoints to create, list, and delete to-do items.
- A BullMQ task queue backed by Redis that processes notification jobs asynchronously without blocking your server.
- A public ngrok URL that exposes your local API to the internet for external testing.
- Secret Mission: Add PUT and PATCH endpoints to update to-do items, demonstrating the difference between full replacement and partial updates.
Are there any prerequisites?
You should have basic JavaScript knowledge and familiarity with JSON. No prior experience with Fastify, Redis, or task queues is needed.
Set Up Your Environment
Before you can build an API, you need the runtime, the message broker, and the tunneling tool that power it.
In this step you will install Node.js, Redis, and ngrok on your machine. Then you will create the project folder and pull in the npm packages your API depends on.
In this step, get ready to:
- Install and verify Node.js, Redis, and ngrok.
- Create your project directory and install dependencies.
- Configure your ngrok account for public tunneling.
Install and verify Node.js, Redis, and ngrok
This project uses three tools that need to be installed on your machine. Node.js runs JavaScript outside the browser, Redis acts as the message broker for background jobs, and ngrok exposes your local server to the internet.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
What about Windows?
This project uses Homebrew, which runs on macOS and Linux. If you are on Windows, install the Windows Subsystem for Linux (WSL) first, then follow the same Homebrew commands inside your WSL terminal.
- Check whether Node.js is already installed by running this command:
node --version
Fastify 5.x requires Node.js 20 or higher. Check the version number that appears in your terminal, then pick the tab below that matches what you see.
✔️ I see version 20 or higher
Your Node.js version is compatible. Move on to the Redis install below.
ⓧ I see an older version
You have Node.js installed, but it is below version 20. Upgrade by running:
- Install the v20 formula with Homebrew by running:
brew install node@20
- After the install finishes, Homebrew will print a caveat that node@20 is keg-only. Link it into your PATH by running:
brew link --overwrite --force node@20
- Confirm the upgrade by running:
node --version
You should now see v20 or higher.
ⓧ Command not found
Node.js is not installed yet. Install it with Homebrew by running:
- Install Node.js by running:
brew install node@20
- After the install finishes, link it into your PATH by running:
brew link --overwrite --force node@20
- Confirm the installation by running:
node --version
You should see v20 or higher.
- Install Redis by running:
brew install redis
- Start the Redis service in the background by running:
brew services start redis
- Verify Redis is running by running:
redis-cli ping
Check what your terminal prints, then pick the tab below.
✔️ I see PONG
Redis is running and ready to accept connections. Move on to the ngrok install below.
ⓧ I see an error or command not found
If redis-cli is not found, re-run brew install redis and check for errors in the Homebrew output.
If you see a connection refused error, the Redis service did not start. Try running brew services restart redis and then redis-cli ping again.
- Install ngrok by running:
brew install ngrok
- Verify ngrok is installed by running:
ngrok help
✔️ I see help output
ngrok is installed and working. You are ready to move on.
ⓧ Command not found
If your terminal does not recognize the command, try closing and reopening your terminal, then run ngrok help again. Homebrew sometimes needs a shell restart for new commands to appear in your PATH.
Create the project directory and install dependencies
Now that your tools are installed, you need a folder for the project code and the npm packages that power your API server.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir todo-api-drill
cd todo-api-drill
- Initialize a new Node.js project by running:
npm init -y
What does npm init -y do?
This command creates a package.json file with default values. The -y flag skips all the interactive prompts and accepts every default.
- Open package.json in your code editor and replace its entire contents with the following:
{
"name": "todo-api-drill",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js",
"worker": "node worker.js"
}
}
What does this code do?
The "type": "module" field tells Node.js to treat .js files as ES Modules, so you can use import and export syntax instead of require().
The scripts section defines shortcut commands. Later you can run npm start to launch your API server and npm run worker to start the background job processor.
- Save package.json.
- Install the three project dependencies by running:
npm install fastify@5.8.5 bullmq@5.78.0 ioredis@5.11.1
What are these packages?
Fastify is a high-performance web framework for building REST APIs. BullMQ provides a task queue system backed by Redis. ioredis is the Redis client that BullMQ uses under the hood to communicate with your local Redis server.
- Confirm the installation succeeded by running:
ls
You should see node_modules, package.json, and package-lock.json in the output.
Don't see node_modules?
Make sure you are inside the todo-api-drill folder when you run npm install. Run pwd to check your current directory.
If you see permission errors, try running the install command again without sudo. Homebrew-installed Node should not require elevated permissions.
Help me troubleshoot my npm install
✔️ Awesome, I've got everything!
Great. Double check you have saved package.json and move on to the next substep.
ⓧ I'd like to double check the full code
Your package.json should look like this after the install (extra fields from npm init are fine as long as these fields are present):
{
"name": "todo-api-drill",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node server.js",
"worker": "node worker.js"
},
"dependencies": {
"bullmq": "5.78.0",
"fastify": "5.8.5",
"ioredis": "5.11.1"
}
}
Sign up for ngrok and configure your authtoken
ngrok needs an account so it can assign you a public URL. The free tier gives you one dev domain per account with no credit card required.
- Open https://ngrok.com in your browser.
- Click Sign up and create a free account (you can sign in with GitHub or Google).
- After signing in, you will land on the ngrok dashboard. Find your authtoken in the getting started section of the dashboard.
- Copy the token, then run the following command in your terminal (replace the placeholder with your actual token):
ngrok config add-authtoken your-authtoken-here
What does this command do?
This saves your authtoken into ngrok's local configuration file so you do not need to pass it every time you start a tunnel. The token is only shown once on the dashboard, so keep it safe.
- Verify the configuration is valid by running:
ngrok config check
You should see output that prints the config file location with no errors.
Seeing a config error?
Make sure you pasted the full authtoken with no extra spaces. You can re-run the ngrok config add-authtoken command again with the correct token to overwrite the previous value.
Help me fix my ngrok config
Your environment is locked and loaded. Next up, you will build the actual To-Do API with routes for creating, listing, and deleting items.
Build the To-Do API
Your environment is set up with Node.js, Redis, and all your project dependencies installed. Now it's time to build the actual API that clients will interact with.
Every to-do app needs a way to create items, list them, and delete them. You'll build these three operations as versioned REST endpoints using Fastify, a high-performance Node.js web framework.
In this step, get ready to:
- Create a Fastify server with three versioned API routes.
- Test all three endpoints using curl to confirm they work.
Create the Fastify server and routes
Your API needs a server that listens for incoming requests and routes them to the right handler. You'll store to-do items in a simple in-memory array for now.
- Create a new file called server.js inside your todo-api-drill folder by running this command:
touch server.js
- Confirm the file exists by running:
ls
You should see server.js listed alongside node_modules and package.json.
- Open server.js in your code editor and add the following code to set up the Fastify instance, your in-memory store, and the GET route:
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
// In-memory storage for to-do items
const todos = [];
let nextId = 1;
// GET /v1/todos - List all to-dos
fastify.get('/v1/todos', async (request, reply) => {
return todos;
});
What does this code do?
import Fastify from 'fastify' loads the Fastify framework using ESM module syntax (enabled by "type": "module" in your package.json).
Fastify({ logger: true }) creates a server instance with built-in structured logging. Every request and response gets logged automatically as JSON in your terminal.
The todos array acts as your database, and nextId assigns a unique ID to each new item. The GET route at /v1/todos simply returns whatever is currently in the array.
Why the /v1/ prefix?
API versioning is a common backend pattern. By prefixing routes with /v1/, you can ship breaking changes later under /v2/ without breaking existing clients. This is a standard interview topic for backend roles.
- Below the GET route, add the POST route that creates new to-do items:
// POST /v1/todos - Create a to-do
fastify.post('/v1/todos', async (request, reply) => {
const { title } = request.body;
if (!title) {
return reply.status(400).send({ error: 'Title is required' });
}
const todo = {
id: nextId++,
title,
completed: false,
};
todos.push(todo);
return reply.status(201).send(todo);
});
What does this code do?
The POST handler extracts title from the request body. If no title is provided, it returns a 400 Bad Request error.
When a valid title arrives, it builds a new to-do object with an auto-incrementing id, the title, and completed: false. It pushes the item into the array and responds with 201 Created plus the new object.
- Below the POST route, add the DELETE route and the start function to finish the file:
// DELETE /v1/todos/:id - Delete a to-do
fastify.delete('/v1/todos/:id', async (request, reply) => {
const id = parseInt(request.params.id, 10);
const index = todos.findIndex((t) => t.id === id);
if (index === -1) {
return reply.status(404).send({ error: 'Todo not found' });
}
const [removed] = todos.splice(index, 1);
return { message: `Todo "${removed.title}" deleted` };
});
// Start server
const start = async () => {
try {
await fastify.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
What does this code do?
The DELETE handler parses the :id from the URL, finds the matching to-do in the array, and removes it with splice(). If no match is found, it returns 404 Not Found.
The start() function boots the server on port 3000. The host: '0.0.0.0' setting makes the server accessible on all network interfaces, which you'll need later when ngrok tunnels traffic to it.
- Save server.js.
- Start the server by running:
node server.js
You should see JSON log output in your terminal containing a message like Server listening at http://0.0.0.0:3000.
Server not starting?
- If you see a syntax error, double-check that your package.json has "type": "module" set. Without it, Node.js cannot use import statements.
- If you see an address-in-use error, another process is already using port 3000. Stop it or change the port in server.js.
- Make sure Redis is still running. The server itself doesn't use Redis yet, but confirm with redis-cli ping for the next step.
Help me debug my Fastify server startup error
✔️ Awesome, I've got everything!
Great. Make sure you've saved server.js and the server is running before moving on.
ⓧ I'd like to double check the full code
Here is the complete server.js file at this point:
import Fastify from 'fastify';
const fastify = Fastify({ logger: true });
const todos = [];
let nextId = 1;
// GET /v1/todos - List all to-dos
fastify.get('/v1/todos', async (request, reply) => {
return todos;
});
// POST /v1/todos - Create a to-do
fastify.post('/v1/todos', async (request, reply) => {
const { title } = request.body;
if (!title) {
return reply.status(400).send({ error: 'Title is required' });
}
const todo = {
id: nextId++,
title,
completed: false,
};
todos.push(todo);
return reply.status(201).send(todo);
});
// DELETE /v1/todos/:id - Delete a to-do
fastify.delete('/v1/todos/:id', async (request, reply) => {
const id = parseInt(request.params.id, 10);
const index = todos.findIndex((t) => t.id === id);
if (index === -1) {
return reply.status(404).send({ error: 'Todo not found' });
}
const [removed] = todos.splice(index, 1);
return { message: `Todo "${removed.title}" deleted` };
});
// Start server
const start = async () => {
try {
await fastify.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
Test your endpoints with curl
Your server is running, but you need to prove each endpoint works correctly. You'll use curl to send HTTP requests directly from a new terminal window.
- Open a new terminal tab or window. Keep the server running in the first terminal.
- Test the GET endpoint to list all to-dos by running:
curl http://localhost:3000/v1/todos
You should see an empty array [] returned, since you haven't added any items yet.
- Test the POST endpoint to create a to-do by running:
curl -X POST -H "Content-Type: application/json" -d '{"title":"Learn BullMQ"}' http://localhost:3000/v1/todos
You should see the created item returned with an ID:
{"id":1,"title":"Learn BullMQ","completed":false}
- Test the DELETE endpoint to remove the to-do you just created by running:
curl -X DELETE http://localhost:3000/v1/todos/1
You should see a success message:
{"message":"Todo \"Learn BullMQ\" deleted"}
- Run the GET request one more time to confirm the array is empty again:
curl http://localhost:3000/v1/todos
You should see [] again, proving the delete worked.
Getting "connection refused"?
- Make sure your server is still running in the other terminal window. If you accidentally closed it, run node server.js again.
- Check that you're using port 3000 in your curl URL. The port must match what's in server.js.
Help me troubleshoot why curl cannot reach my Fastify server
Windows curl and JSON quotes
If you're on Windows and using Command Prompt, replace single quotes with double quotes and escape inner double quotes with a backslash. PowerShell handles this differently again. The commands above work as-is on macOS and Linux.
Your API handles all three CRUD operations perfectly. Next up, you'll add a background job queue so these endpoints can trigger async work without blocking the main request thread.
Add Background Job Processing
Your REST API is live and all three endpoints return the expected JSON responses. Now it is time to solve a real production problem: what happens when your server needs to do something slow (like sending an email) every time a to-do changes?
If the email logic runs inside the request handler, the user waits for the email to finish before they get a response. A task queue fixes this by letting the server drop a job onto a queue and respond immediately, while a separate worker picks up the job in the background.
In this step, get ready to:
- Create a queue module that connects to Redis.
- Wire the queue into your API so every POST and DELETE enqueues a notification job.
- Build a worker process that picks up jobs and logs simulated email notifications.
Create the queue module
The queue module is a small file that creates a BullMQ Queue instance and exports it so both the server and any future workers can share the same queue name.
- Create a new file called queue.js in your todo-api-drill folder by pasting this code:
import { Queue } from 'bullmq';
import IORedis from 'ioredis';
// Connect to local Redis with unlimited retries for blocking commands
const connection = new IORedis({ maxRetriesPerRequest: null });
// Create and export the notifications queue
export const notificationQueue = new Queue('notifications', { connection });
What does this code do?
IORedis creates a connection to your local Redis server (running on the default port 6379).
The maxRetriesPerRequest: null option tells ioredis to retry forever instead of giving up after 20 attempts. BullMQ requires this setting because its workers use blocking Redis commands that may wait a long time for new jobs.
The Queue named notifications is the channel where your server will drop jobs for the worker to pick up.
- Save queue.js.
- Confirm the file exists by running:
ls queue.js
You should see queue.js printed in the output.
Don't see queue.js?
Make sure you saved the file inside the todo-api-drill folder (the same folder that contains server.js and package.json).
help me find where queue.js was saved
Wire the queue into server.js
With the queue module ready, your server needs to import it and enqueue a job every time a to-do is created or deleted. Each job carries the action type and the to-do's title so the worker knows what happened.
- Open server.js in your editor.
- Find this import at the top of the file:
import Fastify from 'fastify';
- Add this line directly below it:
import { notificationQueue } from './queue.js';
- In the POST handler, find the line todos.push(todo); and add this code below it:
await notificationQueue.add('todo-notification', {
action: 'added',
title: todo.title,
id: todo.id,
});
console.log('Job enqueued: todo added');
- In the DELETE handler, find the line const [removed] = todos.splice(index, 1); and add this code below it:
await notificationQueue.add('todo-notification', {
action: 'deleted',
title: removed.title,
id: removed.id,
});
console.log('Job enqueued: todo deleted');
What does this code do?
notificationQueue.add() places a new job on the notifications queue. The first argument ('todo-notification') is the job name. The second argument is the payload object that the worker will receive.
Because this is awaited, the server waits only for the job to be placed on the queue (a fast Redis write), not for the job to be processed. The actual notification work happens later in a separate process.
- Save server.js.
- Stop your currently running server (press Ctrl+C in the terminal where it is running), then restart it by running:
node server.js
- Test by adding a to-do with this command in a separate terminal:
curl -X POST -H "Content-Type: application/json" -d '{"title":"Learn BullMQ"}' http://localhost:3000/v1/todos
In the terminal running your server, you should see Job enqueued: todo added in the log output.
Server crashes on startup?
If you see an error about the notificationQueue import, make sure queue.js is saved in the same folder as server.js. The import path './queue.js' means "same directory."
If you see a connection error mentioning Redis, make sure Redis is still running. Run redis-cli ping and confirm it returns PONG.
help me debug my server.js startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js and confirmed the "Job enqueued" log appears.
ⓧ I'd like to double check the full code
Here is the complete server.js at this point:
import Fastify from 'fastify';
import { notificationQueue } from './queue.js';
const fastify = Fastify({ logger: true });
const todos = [];
let nextId = 1;
// GET /v1/todos - List all to-dos
fastify.get('/v1/todos', async (request, reply) => {
return todos;
});
// POST /v1/todos - Create a to-do
fastify.post('/v1/todos', async (request, reply) => {
const { title } = request.body;
if (!title) {
return reply.status(400).send({ error: 'Title is required' });
}
const todo = {
id: nextId++,
title,
completed: false,
};
todos.push(todo);
await notificationQueue.add('todo-notification', {
action: 'added',
title: todo.title,
id: todo.id,
});
console.log('Job enqueued: todo added');
return reply.status(201).send(todo);
});
// DELETE /v1/todos/:id - Delete a to-do
fastify.delete('/v1/todos/:id', async (request, reply) => {
const id = parseInt(request.params.id, 10);
const index = todos.findIndex((t) => t.id === id);
if (index === -1) {
return reply.status(404).send({ error: 'Todo not found' });
}
const [removed] = todos.splice(index, 1);
await notificationQueue.add('todo-notification', {
action: 'deleted',
title: removed.title,
id: removed.id,
});
console.log('Job enqueued: todo deleted');
return { message: `Todo "${removed.title}" deleted` };
});
// Start server
const start = async () => {
try {
await fastify.listen({ port: 3000, host: '0.0.0.0' });
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
Create the background worker
Jobs are sitting on the queue, but nothing is processing them yet. The worker is a separate Node.js process that listens to the same queue and runs a handler function whenever a new job arrives.
- Create a new file called worker.js in your todo-api-drill folder by pasting this code:
import { Worker } from 'bullmq';
import IORedis from 'ioredis';
// Connect to local Redis (same settings as queue.js)
const connection = new IORedis({ maxRetriesPerRequest: null });
// Process jobs from the 'notifications' queue
const worker = new Worker(
'notifications',
async (job) => {
const { action, title, id } = job.data;
console.log(`EMAIL SENT: To-do #${id} "${title}" was ${action}`);
},
{ connection }
);
// Log when a job completes successfully
worker.on('completed', (job) => {
console.log(`Job ${job.id} completed successfully`);
});
// Log when a job fails
worker.on('failed', (job, err) => {
console.log(`Job ${job.id} failed: ${err.message}`);
});
console.log('Worker is running and waiting for jobs...');
What does this code do?
The Worker constructor takes three arguments: the queue name ('notifications'), a processor function that runs for each job, and a connection config.
Inside the processor, job.data contains the exact payload you passed to notificationQueue.add() in server.js. The worker logs a simulated email instead of actually sending one.
The completed and failed event listeners give you visibility into job outcomes. In production you would replace these with monitoring or alerting logic.
- Save worker.js.
- Open a new terminal tab (keep the server running in the other one).
- Navigate to your project folder and start the worker by running:
cd ~/Desktop/todo-api-drill
node worker.js
You should see Worker is running and waiting for jobs... printed in this terminal.
- Switch to a third terminal and add a to-do by running:
curl -X POST -H "Content-Type: application/json" -d '{"title":"Learn BullMQ"}' http://localhost:3000/v1/todos
Within 1-2 seconds, your worker terminal should print something like:
EMAIL SENT: To-do #1 "Learn BullMQ" was added
Job 1 completed successfully
Worker not printing anything?
Make sure Redis is running (redis-cli ping should return PONG). Both the server and the worker connect to the same Redis instance, so if Redis is down, neither can communicate.
Confirm the queue name matches in both files: queue.js uses 'notifications' and worker.js also uses 'notifications'. A mismatch means the worker is listening to a different queue.
help me debug why my BullMQ worker is not processing jobs
Why a separate process?
Running the worker as its own node worker.js process means it has its own memory and event loop. If a job takes 10 seconds to process, it does not block your API from responding to other requests.
In production, you can scale workers independently. Need more email throughput? Start more worker processes. Your API stays untouched.
Your background job pipeline is fully wired. Next up, you will expose this whole setup to the internet so any external client can hit your endpoints.
Expose Your API with ngrok
Your API handles requests and your worker processes background jobs. The full producer-consumer pipeline works end to end on your machine.
Right now, only your computer can reach the API. In production, APIs need to be accessible over the internet. In this step, you will use ngrok to create a public HTTPS tunnel so any external client can hit your endpoints.
In this step, get ready to:
- Start an ngrok tunnel that exposes port 3000 to the internet.
- Test your API through the public URL and confirm background jobs still process.
Start the ngrok tunnel
Your Fastify server is already listening on port 3000. The ngrok agent creates an encrypted outbound connection to the ngrok cloud, which assigns you a public HTTPS URL and forwards incoming traffic through the tunnel to your local port.
- Open a new terminal window. This is your third terminal (alongside the server and worker terminals).
- Start the ngrok tunnel by running this command:
ngrok http 3000
What does this command do?
The ngrok http 3000 command tells the ngrok agent to forward HTTP traffic from a public URL to your local port 3000. ngrok provisions a TLS certificate automatically, so the public URL uses HTTPS even though your local server is plain HTTP.
- Look at the ngrok terminal output. You should see a console UI confirming the agent is online and forwarding. Find the Forwarding line showing your public HTTPS URL (it looks like https://abc123.ngrok-free.dev).
- Copy your public HTTPS URL. You will use it in the next substep.
Why does the URL change each time?
On ngrok's free tier, a new random URL is assigned every time you restart the tunnel. This is fine for development and testing. Paid plans offer stable custom domains.
Don't see the Forwarding line?
Make sure your authtoken is configured. Run ngrok config check to verify there are no configuration errors.
Confirm your Fastify server is still running on port 3000 in its original terminal. ngrok forwards traffic to whatever is listening on the target port.
help me troubleshoot why ngrok is not showing a Forwarding URL when I run ngrok http 3000
Test your API through the public URL
The real proof is that external requests reach your local server and trigger background jobs exactly like localhost requests do.
- Open another terminal tab (or use the terminal where you previously ran curl).
- List your to-dos through the public URL by running this command:
curl [[NGROK_URL="https://abc123.ngrok-free.dev"]]/v1/todos
- You should see an empty array [] (or your existing to-dos if you added some earlier). The response is identical to hitting http://localhost:3000/v1/todos directly.
- Add a to-do through the public URL by running this command:
curl -X POST -H "Content-Type: application/json" -d '{"title":"Test from the internet"}' [[NGROK_URL="https://abc123.ngrok-free.dev"]]/v1/todos
- You should see the created to-do item returned as JSON with an id, title, and completed field.
- Switch to your worker terminal. You should see the simulated email notification printed within 1-2 seconds:
EMAIL SENT: To-do #1 "Test from the internet" was added
What just happened?
An external request traveled from the internet through ngrok's cloud, down the encrypted tunnel to your local Fastify server on port 3000. Your server enqueued a job to Redis, and your worker picked it up and processed it in the background. The entire architecture works end to end over a public URL.
Getting an error from the public URL?
Make sure your Fastify server is still running on port 3000. If you restarted it, the in-memory to-dos reset, but the endpoints still work.
Check that you copied the full ngrok URL including https:// and did not include a trailing slash before /v1/todos.
If the ngrok tunnel was stopped and restarted, the URL changed. Copy the new Forwarding URL from the ngrok terminal.
help me debug why my curl request to my ngrok public URL is failing
Secret mission
Add PUT and PATCH Endpoints
Your API can create, list, and delete to-dos. But what about updating them? In this mission, you'll implement both PUT (full replacement) and PATCH (partial update) endpoints, a distinction that comes up constantly in backend interviews.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally, so there are no ongoing costs. Redis uses minimal memory and the ngrok tunnel stops the moment you close it.
Resources you used:
- Fastify server process (running on port 3000).
- Worker process (running in a separate terminal).
- ngrok tunnel (forwarding to port 3000).
- Redis server (running via Homebrew services).
- todo-api-drill project folder on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively testing your API or experimenting with new endpoints.
- Your Fastify server stays running on port 3000.
- Your worker keeps processing jobs from the queue.
- Your ngrok tunnel remains active at its public URL.
- Redis continues running in the background via Homebrew services.
Pause - I'll come back to this later
Shut down running processes to free up memory and ports. Your project files stay intact so you can pick up where you left off.
- Switch to the terminal running your ngrok tunnel and press Ctrl+C to stop it.
- Switch to the terminal running your Fastify server and press Ctrl+C to stop it.
- Switch to the terminal running your worker and press Ctrl+C to stop it.
- Stop Redis by running:
brew services stop redis
To resume later, run brew services start redis, then start your server and worker again from the todo-api-drill folder.
Delete - I don't want to use this again
Remove all project resources and start fresh. Your to-do data is in-memory, so it disappears the moment the server stops.
- Switch to the terminal running your ngrok tunnel and press Ctrl+C to stop it.
- Switch to the terminal running your Fastify server and press Ctrl+C to stop it.
- Switch to the terminal running your worker and press Ctrl+C to stop it.
- Stop Redis by running:
brew services stop redis
- Delete the project folder. On macOS/Linux:
cd ~/Desktop
rm -rf todo-api-drill
- On Windows:
cd ~\Desktop
rmdir /s /q todo-api-drill
- Confirm the folder is gone by running ls on your Desktop. You should no longer see todo-api-drill in the output.
Nice Work!
Nice Work!
Nice work! You just built a production-grade backend API with async job processing and exposed it to the internet.
You've learned how to:
- Build a REST API from scratch using Fastify with versioned endpoints (/v1/todos) for creating, listing, and deleting resources.
- Implement async background job processing with BullMQ and Redis so the main request thread never blocks on heavy tasks like sending notifications.
- Expose a local server to the internet using ngrok, proving the producer (API) + consumer (worker) architecture works for external clients.
- Secret Mission: Implemented PUT vs. PATCH semantics to demonstrate understanding of HTTP method differences for full replacement vs. partial update.
Ready to quiz yourself?