Build a URL Shortener API
Build a REST API that shortens URLs, tracks clicks, and handles errors.
Introduction
30 Second Summary
Every time you share a link on social media or in a message, you've probably used a URL shortener. Those tiny links that replace a paragraph-long web address with a few characters are powered by a simple but clever backend service.
In this project, you will build a URL shortener API from scratch using Node.js and Express. You'll create endpoints that accept long URLs, generate unique short codes, redirect visitors, and track click analytics.
What You'll Build
Paste a long URL into your terminal, get back a short code, hit that code's endpoint, and watch your browser redirect to the original page while a click counter ticks up behind the scenes.
By the end of this project, you'll have:
- A working POST endpoint that accepts any valid URL and returns a unique 7-character short code you can share instantly.
- A redirect route that sends visitors from the short code to the original URL, counting every click along the way.
- A stats endpoint you can query to see how many times each short link has been visited.
- Production-style validation middleware and error handling that reject bad input and catch unexpected failures with proper HTTP status codes.
- Secret Mission: Add expiration dates to your short URLs so they auto-expire after a configurable time window.
Are there any prerequisites?
This project assumes intermediate JavaScript knowledge (async/await, objects, arrays). You should already have Node.js and a code editor (like VS Code or Cursor) installed on your machine.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up the Project
To build a URL shortener, you need a running server that can receive requests and send responses. Before writing any server code, you need a properly configured Node.js project with the right tools installed.
In this step, you will create a project from scratch, install Express (the framework that handles HTTP requests), and get a minimal server responding to requests on your machine.
In this step, get ready to:
- Create and configure a Node.js project with ES modules.
- Install Express as your HTTP framework.
- Write and run a minimal server that responds with JSON.
Create and configure the project
Every Node.js project starts with a folder and a package.json file that tracks your project's name, dependencies, and scripts.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir url-shortener
cd url-shortener
- Initialize a new Node.js project by running:
npm init -y
You should see output confirming that a package.json file was created in your folder.
What does npm init -y do?
This command creates a package.json file with default values. The -y flag skips all the interactive questions and accepts defaults automatically.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the url-shortener folder, then click Open.
You should see package.json listed in the VS Code Explorer sidebar on the left.
- Click on package.json in the Explorer sidebar to open it.
- Select all the content in the file and replace it with the following:
{
"name": "url-shortener",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js"
}
}
What did you change?
- "type": "module" enables ES modules so you can use modern import/export syntax instead of the older require() style.
- The dev script uses node --watch to automatically restart the server whenever you save a file.
- The start script runs the server normally without watching for changes.
- Save the file with Cmd+S (macOS) or Ctrl+S (Windows).
Not seeing package.json in the sidebar?
Make sure you opened the url-shortener folder itself, not the Desktop folder. The Explorer sidebar should show url-shortener as the top-level heading.
If your Explorer sidebar is collapsed, click the top icon in the left sidebar (two stacked pages) to expand it.
Help me find my package.json in VS Code
Install Express
Express is the framework that handles incoming HTTP requests and routes them to your code. It is the most widely used Node.js web framework.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Install Express by running:
npm install express@5.2.1
You should see output confirming that Express was added, and a new node_modules folder appears in your Explorer sidebar.
What did npm install do?
This command downloaded Express 5.2.1 and all its sub-dependencies into a node_modules folder. It also added Express to the dependencies section of your package.json automatically.
Seeing a permissions error or EACCES?
If the install fails with a permissions error, make sure you are running the terminal inside the url-shortener folder. Run pwd to check your current location. It should end with /url-shortener.
Help me fix my npm install error
Create the server and verify it runs
Now you will write the actual server code. This file will import Express, create an app, define a route, and start listening for requests.
- In the VS Code Explorer sidebar, hover over the url-shortener heading and click the New File icon (a page with a plus sign).
- Name the file server.js and press Enter.
- Paste the following code into server.js:
import express from 'express';
// Create the Express app and set the port
const app = express();
const PORT = 3000;
// Health check route - confirms the API is running
app.get('/', (req, res) => {
res.json({ message: 'URL Shortener API is running' });
});
// Start the server
app.listen(PORT, () => {
console.log(`URL Shortener API running at http://localhost:${PORT}`);
});
What does this code do?
- import express from 'express' loads the Express framework using ES module syntax.
- express() creates a new Express application instance that you attach routes to.
- app.get('/', ...) defines a route that responds to GET requests at the root path with a JSON message.
- app.listen(PORT, ...) starts the server on port 3000 and prints a confirmation URL to the terminal.
- Save server.js with Cmd+S (macOS) or Ctrl+S (Windows).
- Start the server by running this command in your terminal:
npm run dev
You should see this output in your terminal:
URL Shortener API running at http://localhost:3000
Why npm run dev instead of node server.js?
The dev script uses node --watch, which automatically restarts the server every time you save a change. This means you never have to manually stop and restart during development.
The server is now running and occupying this terminal. You need a second terminal to send requests to it.
- Open a new terminal panel by clicking the + icon in the top-right corner of the terminal panel in VS Code.
Before you run the next command, what do you think the server will respond with when you hit the root path?
- Test your server by running:
curl http://localhost:3000
You should see this JSON response:
{"message":"URL Shortener API is running"}
Seeing 'Connection refused' or no response?
- Check your first terminal panel. If you do not see the "URL Shortener API running" message, the server did not start. Look for error messages there.
- If you see "Cannot find module", make sure server.js is saved in the root of your url-shortener folder (not inside a subfolder).
- If you see "SyntaxError: Cannot use import statement", confirm that your package.json includes "type": "module".
Help me fix my server startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved both package.json and server.js before moving on.
ⓧ I'd like to double check the full code
Here is what each file should look like at this point.
package.json:
{
"name": "url-shortener",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js"
},
"dependencies": {
"express": "5.2.1"
}
}
server.js:
import express from 'express';
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.json({ message: 'URL Shortener API is running' });
});
app.listen(PORT, () => {
console.log(`URL Shortener API running at http://localhost:${PORT}`);
});
Your project is configured and your server is running. Next up, you will build the endpoint that actually shortens URLs.
Build the Shorten Endpoint
Your server is running and responding to requests. Now it's time to build the feature that makes a URL shortener actually useful.
The core value of a URL shortener is accepting a long URL and returning a short code. In this step, you'll create a POST endpoint that takes in any URL and hands back a unique 7-character code.
In this step, get ready to:
- Install nanoid and configure the server to parse JSON bodies.
- Build a POST /shorten endpoint that generates unique short codes.
- Test the endpoint with curl to confirm everything works.
Install nanoid
Your POST endpoint will need to generate unique, URL-safe short codes. nanoid is a tiny library that produces cryptographically secure random IDs, perfect for this job.
- Install nanoid by running this command in your terminal:
npm install nanoid@5.1.16
You should see output confirming the package was added.
Install failing?
Make sure you're running the command inside the url-shortener folder where your package.json lives. Check your internet connection if you see a network error.
help me fix my npm install error for nanoid
Add the nanoid import and in-memory store
Before you can use nanoid in your route, you need to import it. You also need a place to store URL mappings and a way to parse incoming JSON request bodies.
- In server.js, update the top of your file to add the nanoid import, an in-memory store object, and the express.json() middleware. Your file's top section should now look like this:
import express from 'express';
import { nanoid } from 'nanoid';
const app = express();
const PORT = 3000;
// In-memory store for URL mappings
const urlStore = {};
// Parse JSON bodies
app.use(express.json());
What does this code do?
- import { nanoid } from 'nanoid' brings in the ID generator you just installed.
- urlStore is a plain JavaScript object that acts as your database. Each short code becomes a key pointing to its URL data.
- express.json() is middleware that automatically parses JSON request bodies and puts the result in req.body.
- Save server.js.
- Check your terminal. The server restarts automatically (thanks to --watch) and prints URL Shortener API running at http://localhost:3000 again with no errors.
Seeing an error about nanoid?
If you see Cannot find package 'nanoid', make sure you ran the install command from the previous substep. Check that nanoid appears in your package.json under dependencies.
help me fix a nanoid import error in my Express server
Create the POST /shorten route
Now for the main event. This route accepts a URL in the request body, generates a short code, saves the mapping, and sends back the result.
- In server.js, find the existing GET / route. Add a // Health check comment above it, then paste the following POST route directly below it:
// Shorten a URL
app.post('/shorten', (req, res) => {
const { url } = req.body;
const shortCode = nanoid(7);
urlStore[shortCode] = {
url,
shortCode,
clicks: 0,
createdAt: new Date().toISOString()
};
res.status(201).json({
shortCode,
url,
shortUrl: `http://localhost:${PORT}/${shortCode}`
});
});
What does this route do?
- const { url } = req.body pulls the URL out of the JSON body the client sent.
- nanoid(7) generates a random 7-character string like V1StGXR. Each call produces a unique code.
- The mapping stored in urlStore tracks the original URL, the short code, a click counter starting at 0, and a creation timestamp.
- res.status(201).json(...) responds with HTTP 201 (Created) and a JSON body containing the short code and a ready-to-use short URL.
- Save server.js.
- Your terminal should show the server restarting with no errors.
Before you test, what do you think will happen when you POST a URL to /shorten? What status code and data do you expect back?
- Test your new endpoint by running this curl command:
curl -X POST http://localhost:3000/shorten -H "Content-Type: application/json" -d "{\"url\": \"https://www.wikipedia.org/wiki/Node.js\"}"
You receive a 201 response with a JSON body containing a 7-character shortCode, the original url, and a full shortUrl you can use later.
What should I see?
Your response looks something like this (your short code will be different):
{
"shortCode": "V1StGXR",
"url": "https://www.wikipedia.org/wiki/Node.js",
"shortUrl": "http://localhost:3000/V1StGXR"
}
Getting an empty response or error?
- If you see Cannot destructure property 'url' of undefined, make sure app.use(express.json()) appears above the POST route in your file.
- If curl returns nothing, confirm your server is still running in the terminal (look for the startup message).
- Double check that -H "Content-Type: application/json" is included in your curl command. Without it, Express won't parse the body.
help me debug my POST /shorten endpoint
✔️ Awesome, I've got everything!
Great. Double check you've saved server.js and your server is still running.
ⓧ I'd like to double check the full code
Your server.js should contain these sections in order. Scroll up and confirm each is present:
- Both imports (express and nanoid) at the top.
- The app and PORT declarations.
- const urlStore = {} for the in-memory store.
- app.use(express.json()) for parsing JSON bodies.
- The GET / health check route (with // Health check comment).
- The POST /shorten route (with // Shorten a URL comment).
- The app.listen call at the bottom.
Your shorten endpoint is live. You can already POST any URL and get a unique short code back. Next up, you'll make those short codes actually do something by building the redirect endpoint.
Build the Redirect Endpoint
In the previous step, you built the POST /shorten endpoint that accepts a long URL and hands back a short code. But right now that short code does nothing.
A URL shortener only delivers value when the short code actually sends people somewhere. In this step, you will create the redirect endpoint that looks up the short code, counts the click, and sends the user to the original URL using an HTTP redirect.
In this step, get ready to:
- Create a GET route that redirects short codes to the original URL.
- Return a 404 error when a short code does not exist.
- Test the redirect with curl and confirm click tracking works.
Add the redirect route
When a user visits a short URL like http://localhost:3000/abc1234, Express needs a route that captures that code from the URL, looks it up in the store, and sends the user onward.
- In server.js, find the app.listen() call at the bottom of your file.
- Add the following redirect route directly above app.listen():
// Redirect to original URL
app.get('/:code', (req, res) => {
const { code } = req.params;
const entry = urlStore[code];
if (!entry) {
return res.status(404).json({ error: 'Short URL not found' });
}
entry.clicks += 1;
res.redirect(302, entry.url);
});
What does this code do?
This route uses a URL parameter (:code) to capture whatever appears after the slash. Express makes it available via req.params.code.
- urlStore[code] looks up the short code in your in-memory store.
- If the code does not exist, the route immediately returns a 404 JSON error and stops.
- If found, it increments the clicks counter by 1, then calls res.redirect(302, entry.url) to send the user to the original URL.
A 302 status tells the browser "this resource is temporarily at another location." The browser then follows the Location header to the original URL.
- Save server.js.
- Check your terminal. You should see the server restart automatically thanks to --watch.
Server not restarting?
Make sure you saved the file. If the server is not running, start it again with:
npm run dev
If you see a syntax error in the terminal, check that the new route has matching curly braces and parentheses.
help me fix a syntax error in my Express redirect route
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js and the server restarted in your terminal.
ⓧ I'd like to double check the full code
Your server.js should now contain these sections in this order:
- Imports (express and nanoid).
- App setup (const app, PORT, urlStore).
- express.json() middleware.
- GET / health check route.
- POST /shorten route.
- GET /:code redirect route (the one you just added).
- app.listen() at the very bottom.
Scroll up and confirm each section is present in your file, in this order.
Test the redirect
When the server restarts, it clears the in-memory store. You need to create a fresh short URL before you can test the redirect.
- Create a new short URL by running this command:
curl -X POST http://localhost:3000/shorten -H "Content-Type: application/json" -d "{\"url\": \"https://www.wikipedia.org/wiki/Node.js\"}"
You should see a JSON response containing a shortCode (7 characters) and the shortUrl. Copy the shortCode value from the response.
Before you test the redirect, think about what you expect to happen. When you hit the short code endpoint, what HTTP status and headers should curl show you?
- Test the redirect by running this command (replace the short code with your own):
curl -i http://localhost:3000/[[SHORTCODE="your-short-code"]]
Why the -i flag?
The -i flag tells curl to include the HTTP response headers in the output. Without it, you would only see the response body and miss the 302 status and Location header that prove the redirect is working.
You should see HTTP status 302 and a Location: https://www.wikipedia.org/wiki/Node.js header in the output. That Location header is what tells the browser where to go next.
- Now test what happens with a code that does not exist:
curl -i http://localhost:3000/fakecode
You should see a 404 status with the JSON body {"error":"Short URL not found"}.
Not seeing a 302 response?
If you see a 404 when using a valid code, the most common cause is that the server restarted between your POST and your GET. The in-memory store is cleared on every restart. Run the POST command again to create a fresh short URL, then immediately test the redirect with the new code.
If you see no response at all, confirm the server is still running in your terminal.
help me debug my redirect endpoint
Your short codes are alive. Hit a valid code and you get redirected. Hit a fake one and you get a clean 404 error. Next up, you will add input validation so bad URLs get rejected before they ever reach the store.
Add Validation Middleware and Stats
Right now, your POST /shorten endpoint happily accepts anything you throw at it. An empty body, a missing URL field, a string like "not-a-url".
Real APIs validate input before processing it. In this step, you'll create a reusable validation middleware that rejects bad requests with proper HTTP error codes. You'll also add a stats endpoint so you can query click data without triggering a redirect.
In this step, get ready to:
- Create a validateUrl middleware function that rejects bad input with 400 errors.
- Add a GET /stats/:code endpoint that returns click data without redirecting.
- Test both features with curl to confirm they work.
Create the URL validation middleware
In Express, a middleware function sits between the incoming request and your route handler. It can inspect the request, reject it early, or pass it along to the next handler in the chain.
- In server.js, find the line that says app.use(express.json());.
- Add the following validation middleware below it:
// --- Validation Middleware ---
function validateUrl(req, res, next) {
const { url } = req.body;
if (!url || typeof url !== 'string') {
return res.status(400).json({ error: 'URL is required' });
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
return res.status(400).json({ error: 'Invalid URL. Must start with http:// or https://' });
}
next();
}
What does this code do?
- The function takes three parameters: req, res, and next. Calling next() passes control to the actual route handler.
- The first check ensures url exists and is a string. If not, the request is rejected with a 400 status.
- The second check ensures the URL starts with http:// or https://. This prevents storing values that aren't actual web addresses.
- If both checks pass, next() is called and the request continues to the route handler.
Now you need to attach this middleware to your POST /shorten route so it runs before the handler.
- Find the line that starts with app.post('/shorten',:
app.post('/shorten', (req, res) => {
- Add validateUrl as the second argument, so the line now looks like this:
app.post('/shorten', validateUrl, (req, res) => {
- Save server.js.
- Test the validation by sending an invalid URL:
curl -X POST http://localhost:3000/shorten -H "Content-Type: application/json" -d "{\"url\": \"not-a-url\"}"
You should see a 400 response with the error message:
{"error":"Invalid URL. Must start with http:// or https://"}
Still getting a 201 response?
- Make sure validateUrl appears between '/shorten' and (req, res) => in the route definition. It must be a separate argument separated by commas.
- Confirm that your validateUrl function is defined above the route that uses it. JavaScript reads top-to-bottom.
- Check that node --watch restarted the server after you saved. You should see output in your terminal confirming the restart.
Help me debug this
Add the stats endpoint
You need a way to check how many times a short URL has been clicked without triggering a redirect. A dedicated stats route solves this.
- In server.js, find the comment that says // Redirect to original URL.
- Add the following route directly above that comment:
// Get stats for a short URL
app.get('/stats/:code', (req, res) => {
const { code } = req.params;
const entry = urlStore[code];
if (!entry) {
return res.status(404).json({ error: 'Short URL not found' });
}
res.json(entry);
});
What does this code do?
- The route /stats/:code uses a URL parameter (:code) to look up the stored entry.
- If the short code exists, it returns the full entry object (url, shortCode, clicks, createdAt) as JSON.
- This route does NOT increment clicks or redirect. It is purely informational.
Placing /stats/:code above /:code is critical. Express matches routes top-to-bottom, and /:code would catch /stats/abc123 as a code lookup if it came first.
- Save server.js.
- First, create a short URL to test with:
curl -X POST http://localhost:3000/shorten -H "Content-Type: application/json" -d "{\"url\": \"https://www.wikipedia.org/wiki/Node.js\"}"
- Copy the shortCode value from the response.
- Check the stats for your new short URL by running:
curl http://localhost:3000/stats/[[SHORT_CODE="your 7-character short code"]]
You should see JSON with clicks set to 0 since you haven't redirected through it yet.
Getting a 404 from the stats endpoint?
- Make sure /stats/:code is defined ABOVE /:code in your file. If the catch-all route is first, it intercepts the request.
- The in-memory store resets every time the server restarts. If node --watch restarted between creating and querying, your short code no longer exists. Create a new one and try again.
Help me figure out why my stats endpoint isn't working
Verify validation and stats together
Before you run these final tests, think about what you expect to see. What HTTP status code should come back when you POST with no body at all?
- Test sending a request with no body:
curl -X POST http://localhost:3000/shorten
You should see a 400 response with {"error":"URL is required"}.
- Now hit the redirect endpoint a couple of times to build up clicks:
curl -i http://localhost:3000/[[SHORT_CODE="your 7-character short code"]]
- Run it two or three times, then check the stats:
curl http://localhost:3000/stats/[[SHORT_CODE="your 7-character short code"]]
The clicks value in the response should match exactly how many times you hit the redirect endpoint.
✔️ 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
Your server.js should now contain these sections in order. Scroll up and confirm each one is present:
- Imports (express and nanoid).
- App setup (app, PORT, urlStore).
- JSON parsing middleware (app.use(express.json())).
- The validateUrl middleware function.
- GET / health check route.
- POST /shorten with validateUrl as the second argument.
- GET /stats/:code route.
- GET /:code redirect route (must be last of the routes).
- app.listen at the bottom.
Your API now rejects garbage input and exposes click analytics. Next up, you'll add request logging and centralized error handling to make this feel like a production service.
Add Request Logging and Error Handling
Your API shortens URLs, redirects, validates input, and reports stats. But right now, you have no visibility into what requests are hitting your server.
Without logging, debugging production issues is guesswork. Without centralized error handling, an unexpected failure crashes the server or leaks a raw stack trace to users. These two middleware patterns are what separate a hobby project from a production one.
In this step, get ready to:
- Add a request logger that prints every incoming request to the terminal.
- Add centralized error-handling middleware that catches unexpected failures.
- Fix route ordering so the catch-all route does not intercept named paths.
Add request logging middleware
Express middleware runs in the order you register it. A logger registered at the top of the chain sees every single request before any route handles it.
- In server.js, find the line that says // Parse JSON bodies.
- Add the following logger middleware directly above that line:
// --- Middleware ---
// Request logger
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${req.method} ${req.path}`);
next();
});
What does this code do?
- app.use(...) registers middleware that runs on every request, regardless of the HTTP method or path.
- new Date().toISOString() creates a precise timestamp like 2026-08-06T12:00:00.000Z.
- req.method and req.path give you the HTTP verb (GET, POST) and the URL path.
- next() passes control to the next middleware or route. Without it, the request hangs forever.
- Save server.js.
- Test the logger by running this command in a second terminal:
curl http://localhost:3000/
- Switch back to the terminal running your server. You should see a log line like [2026-08-06T12:00:00.000Z] GET / printed to the console.
Not seeing a log line?
- Make sure you placed the logger ABOVE app.use(express.json()). Middleware runs top-to-bottom, so a logger placed below your routes never fires for those routes.
- Confirm you saved the file. The --watch flag auto-restarts the server on save, but unsaved changes are ignored.
- help me debug my request logger middleware
Add error-handling middleware and a test route
Express identifies error-handling middleware by its 4-parameter signature: (err, req, res, next). All four parameters must be present, even if you do not use next. This middleware must be defined after all your routes.
- In server.js, scroll to the bottom of the file and find the app.listen call.
- Add the error-handling middleware directly above app.listen:
// --- Error Handling Middleware ---
app.use((err, req, res, next) => {
console.error(`[ERROR] ${err.message}`);
res.status(500).json({ error: 'Internal server error' });
});
Why four parameters?
Express uses the argument count to distinguish error handlers from regular middleware. If you accidentally omit next, Express treats it as a normal 3-parameter middleware and it will never receive errors.
- Now add a test route that deliberately throws an error. Add this directly after the app.get('/:code', ...) route (at the end of your routes, before the error-handling middleware):
// Test error route
app.get('/error', (req, res) => {
throw new Error('Test error');
});
What does this route do?
This route exists solely to test your error handler. When someone hits GET /error, the synchronous throw is automatically caught by Express and forwarded to the error-handling middleware.
- Save server.js.
- Before you test, think about this: if a request comes in for /error, which route do you think Express will match first?
- Test it by running:
curl -i http://localhost:3000/error
You get a 404 response with {"error":"Short URL not found"} instead of a 500. That is not a bug in your code. It is a route ordering problem.
What just happened?
Express matches routes top-to-bottom. Your /:code route is defined BEFORE /error. When Express sees GET /error, the /:code pattern matches "error" as a valid code parameter. It looks up "error" in the store, finds nothing, and returns 404.
Your /error route never gets a chance to run. The catch-all /:code swallows it first.
Fix the route order
The fix is simple: any named route must come BEFORE the /:code catch-all. The catch-all should always be the last route defined.
- In server.js, find the test error route you just added (after /:code):
// Test error route
app.get('/error', (req, res) => {
throw new Error('Test error');
});
- Cut that entire block and move it above the // Redirect to original URL comment. It should sit between GET /stats/:code and GET /:code.
After the move, your route order should look like this:
// Get stats for a short URL
app.get('/stats/:code', (req, res) => {
// ... stats logic
});
// Test error route
app.get('/error', (req, res) => {
throw new Error('Test error');
});
// Redirect to original URL
app.get('/:code', (req, res) => {
// ... redirect logic
});
- Also add a // --- Routes --- comment above the // Health check line, and a // --- Start Server --- comment above app.listen to keep the file organized.
- Save server.js.
- Now predict: what do you expect to see when you hit /error this time?
- Test again by running:
curl -i http://localhost:3000/error
This time you see a 500 status with {"error":"Internal server error"}. Your error handler caught the thrown exception and responded cleanly.
- Check your server terminal. You should see both the log line [...] GET /error AND the error log [ERROR] Test error printed below it.
- Confirm the other routes still work by running:
curl http://localhost:3000/
- You should see {"message":"URL Shortener API is running"} and a corresponding log line in your server terminal.
Still getting 404 on /error?
- Double check that app.get('/error', ...) appears ABOVE app.get('/:code', ...) in your file. Express matches the first route that fits.
- Confirm you saved the file after moving the route.
- help me fix my Express route ordering problem with the catch-all route
✔️ Awesome, I've got everything!
Great. Double check you saved server.js and that every request you make now prints a log line in your terminal.
ⓧ I'd like to double check the full code
Scroll through your server.js and confirm these sections appear in this order:
- Imports (express, nanoid) and constants (app, PORT, urlStore).
- // --- Middleware --- section: request logger, then express.json().
- // --- Validation Middleware --- section: validateUrl function.
- // --- Routes --- section: GET /, POST /shorten, GET /stats/:code, GET /error, then GET /:code (last).
- // --- Error Handling Middleware --- section: the 4-parameter error handler.
- // --- Start Server --- section: app.listen.
Your API is now production-ready: every request leaves a paper trail, and any unexpected error gets a clean JSON response instead of crashing the server. You built a complete URL shortener with validation, logging, and error handling from scratch.
Secret mission
Add URL Expiration
Real URL shorteners like Bitly let links expire after a set time. Can you add a time dimension to your API so short URLs automatically stop working after a configurable window? You will modify the data model, add conditional expiration logic, and handle the edge case of expired links with a proper 410 Gone response.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference, pause the running server, or delete everything entirely.
Resources you used:
- The url-shortener project folder on your Desktop (contains server.js, package.json, and node_modules).
Keep everything running
No action needed. Choose this if you are still actively building or want to keep the project for future reference.
- Your server continues running via npm run dev. You can stop it anytime with Ctrl+C in the terminal.
- The project folder stays on your Desktop ready to extend.
Pause - I'll come back to this later
Stop the running server to free up the terminal and port 3000. Your files stay intact for when you return.
- In your terminal, press Ctrl+C to stop the running server.
- When you come back, open the url-shortener folder in your editor and run npm run dev to start the server again.
Delete - I don't want to use this again
Remove all project files from your machine. No cloud cleanup is needed since everything runs locally.
- In your terminal, press Ctrl+C to stop the running server.
- Navigate to your Desktop and delete the project folder by running these commands:
cd ~/Desktop
rm -rf url-shortener
On Windows, use this command instead:
cd ~\Desktop
rmdir /s /q url-shortener
- Confirm the folder is gone by running:
ls
You should no longer see url-shortener in the output.
Nice Work!
Nice Work!
You just built a production-structured URL shortener API from scratch using Node.js and Express. You can shorten any URL, redirect users with click tracking, and reject bad input with proper HTTP status codes.
You've learned how to:
- Built a complete REST API with Express routing, including HTTP redirects (302) that track clicks and nanoid-generated short codes.
- Created reusable validation middleware that rejects bad input with proper status codes, and a stats endpoint that exposes click data without triggering redirects.
- Added request logging and centralized error handling middleware, and structured routes to avoid catch-all conflicts.
- Secret Mission: Added time-based URL expiration with 410 Gone responses for expired links.
Ready to quiz yourself?