Build a URL Shortener
Build a URL shortener with Flask and Redis to learn system design.
Introduction
30 Second Summary
Every time you click a shortened link on social media or in an email, a system instantly translates that tiny code back into the full web address. This happens billions of times a day, and the design behind it is one of the most popular questions in system design interviews.
In this project, you will build a working URL shortener from scratch using Python, Flask, and Redis. You will shorten URLs, redirect users, and track clicks while learning the system design patterns that power services like bit.ly.
What You'll Build
A local URL shortener API that accepts long URLs, generates compact short codes using Base62 encoding, stores mappings in Redis, and redirects visitors with click tracking.
By the end of this project, you'll have:
- A Flask REST API with endpoints for shortening URLs and redirecting users.
- A Base62 encoding module that converts numeric IDs into compact, URL-safe short codes.
- A Redis-backed storage layer with atomic ID generation and fast O(1) lookups for redirects.
- Secret Mission: Add URL expiration with Redis TTL so short links automatically expire after a configurable time period.
Are there any prerequisites?
No prior experience with Flask or Redis is needed. This guide walks you through installing and setting up every tool from scratch, including Python, Redis for Windows, and VS Code.
Install Python and Redis
Your URL shortener needs two core pieces of infrastructure: Python to run the web server, and Redis to store URL mappings with fast lookups.
In this step, you'll install both tools, create a project folder, and set up a clean Python environment with all the packages your project needs.
In this step, get ready to:
- Install Python 3.9+ and verify it works in Command Prompt.
- Download and run Redis for Windows.
- Create a project folder with a virtual environment and install Flask and redis-py.
Install and verify Python
Python is the language you'll use to build your Flask API. You need version 3.9 or higher installed on your machine.
- Open Command Prompt by pressing the Windows key, typing cmd, and pressing Enter.
- Check your Python version by running this command:
python --version
You should see a version number printed in the terminal.
✔️ I see version 3.9 or higher
Your Python installation is ready. Move on to the next substep.
ⓧ I see a version lower than 3.9
You have an older version of Python that won't work with the packages this project uses. You need to install a newer version.
- Go to python.org/downloads in your browser.
- Download the latest Python installer for Windows.
- Run the installer. On the first screen, check the box that says Add python.exe to PATH, then click Install Now.
- Close your current Command Prompt and open a new one.
- Run python --version again to confirm it now shows 3.9 or higher.
Still seeing the old version?
The new installer may not have overridden the old PATH entry. Restart your computer, then open a fresh Command Prompt and try again.
help me fix my Python version on Windows
ⓧ 'python' is not recognized
Python is not installed on your machine yet. Let's install it now.
- Go to python.org/downloads in your browser.
- Download the latest Python installer for Windows.
- Run the installer. On the first screen, check the box that says Add python.exe to PATH, then click Install Now.
- Close your current Command Prompt and open a new one.
- Run python --version to confirm it shows 3.9 or higher.
Why check the PATH box?
Adding Python to your PATH lets you run python from any Command Prompt window. Without it, Windows won't know where to find the Python executable.
Still not recognized after installing?
Make sure you opened a new Command Prompt after the install. If that doesn't help, restart your computer to refresh system PATH variables.
help me get Python recognized in Command Prompt
Download and start Redis
Redis is an in-memory key-value store that gives you sub-millisecond lookups. Your URL shortener will use it to store the mapping between short codes and original URLs.
Since Redis doesn't ship an official Windows build, you'll use a community-maintained Windows port.
- Open the Redis for Windows releases page by visiting this link in your browser:
- https://github.com/redis-windows/redis-windows/releases.
- Find the latest release and download the file named Redis-8.6.0-Windows-x64-msys2-with-Service.zip (the version number may be higher if a newer release is available. Look for the file that ends in msys2-with-Service.zip).
- Extract the zip file to C:\redis. You should see files like RedisService.exe and redis-cli.exe directly inside C:\redis.
Extract to the right level
Some zip tools create a nested folder when extracting. Make sure RedisService.exe is at C:\redis\RedisService.exe, not inside a deeper subfolder.
Now start the Redis server. It needs to keep running in its own Command Prompt window for the rest of this project.
- Open a new Command Prompt window (keep it separate from the one you used for Python).
- Navigate to the Redis folder and start the server by running these commands:
cd C:\redis
RedisService.exe run --foreground
What does this command do?
cd C:\redis moves your terminal into the folder where you extracted Redis.
RedisService.exe run --foreground starts the Redis server and keeps it visible in this window so you can see its log output. This window must stay open while you work on the project.
You should see log output indicating Redis is ready to accept connections on port 6379.
- Open a third Command Prompt window and verify Redis is responding by running:
C:\redis\redis-cli.exe ping
You should see PONG printed in the terminal. This confirms Redis is running and accepting commands.
Don't see PONG?
Check the Command Prompt window where you ran RedisService.exe. If it closed or shows an error, try running it again.
Make sure you're using the full path C:\redis\redis-cli.exe in your verification command.
help me get Redis running on Windows
Create the project folder and install dependencies
With Python and Redis ready, you need a dedicated project folder and a virtual environment to keep your project's packages isolated from your system Python.
- Press the Windows key, type VS Code, and press Enter to open VS Code.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, click New Folder, name it url-shortener, then select it and click Select Folder.
- Open the integrated terminal by pressing Ctrl+` (backtick key, usually above Tab).
Your terminal should show you're inside the url-shortener folder.
- Create a virtual environment by running this command:
python -m venv venv
What is a virtual environment?
A virtual environment is an isolated copy of Python for your project. Packages you install inside it won't affect other Python projects on your machine.
The command creates a venv folder inside your project that contains its own Python executable and a place to install packages.
- Activate the virtual environment by running:
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
- Install Flask and the Redis Python client by running:
pip install flask==3.1.3 redis==6.3.0
What are these packages?
flask==3.1.3 is a lightweight web framework for building your API endpoints.
redis==6.3.0 is the Python client library that lets your code talk to the Redis server you started earlier.
You should see pip downloading and installing both packages along with their dependencies. Look for a success message at the end.
Install failing?
Make sure you see (venv) in your terminal prompt. If it's missing, run venv\Scripts\activate again before retrying the install command.
If you see a permissions error, make sure you're running inside the virtual environment and not trying to install system-wide.
help me fix pip install errors in my virtual environment
Your development environment is ready. You have Python, Redis, and all the packages installed. Next up, you'll build the Base62 encoding module that turns numeric IDs into compact short codes.
Build the Base62 Encoding Module
Your environment is ready with Python, Redis, and Flask all installed. Now it's time to build the core algorithm that makes URL shortening possible.
Every URL shortener needs a way to convert a numeric ID into a short, shareable string. In this step, you'll build a Base62 encoding module that takes an integer and outputs a compact alphanumeric code. This is the same encoding strategy that real URL shorteners use to keep codes short and URL-safe.
In this step, get ready to:
- Create a Base62 encoding module that converts integers to short codes.
- Test the encoder to verify correct outputs.
- Understand why Base62 is the right encoding choice for URL shorteners.
Create the Base62 encode function
Base62 uses 62 characters (digits, lowercase letters, and uppercase letters) to represent numbers compactly. A six-character Base62 code can represent over 56 billion unique values.
- In VS Code, create a new file called encoder.py inside your url-shortener folder.
- Add the Base62 character set and the encode function by pasting this code:
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def base62_encode(number):
"""Convert a positive integer to a Base62 string."""
if number == 0:
return BASE62[0]
result = []
while number > 0:
number, remainder = divmod(number, 62)
result.append(BASE62[remainder])
return "".join(reversed(result))
What does this code do?
BASE62 defines the 62-character alphabet: digits 0-9, then lowercase a-z, then uppercase A-Z. Every character maps to a position (0 through 61).
base62_encode() repeatedly divides the input number by 62. Each remainder becomes one character in the output code. The result is reversed because remainders come out in least-significant-first order.
- Save encoder.py.
File not saving?
Make sure the file is saved directly inside the url-shortener folder, not inside the venv subfolder. Check the VS Code Explorer sidebar to confirm encoder.py appears at the top level alongside the venv folder.
help me figure out why my file is in the wrong location
Add the decode function
The decode function reverses the process. It takes a short code string and converts it back to the original integer.
- In encoder.py, add the decode function below your existing code:
def base62_decode(short_code):
"""Convert a Base62 string back to a positive integer."""
number = 0
for char in short_code:
number = number * 62 + BASE62.index(char)
return number
What does this code do?
base62_decode() walks through each character in the short code and multiplies the running total by 62 before adding the character's position. This reconstructs the original number from left to right.
- Save encoder.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def base62_encode(number):
"""Convert a positive integer to a Base62 string."""
if number == 0:
return BASE62[0]
result = []
while number > 0:
number, remainder = divmod(number, 62)
result.append(BASE62[remainder])
return "".join(reversed(result))
def base62_decode(short_code):
"""Convert a Base62 string back to a positive integer."""
number = 0
for char in short_code:
number = number * 62 + BASE62.index(char)
return number
Test the encoder in a Python shell
Before wiring this into your API, you should verify the encoder produces the expected output for known inputs.
- In your VS Code terminal (with your virtual environment still active), open a Python interactive shell by running:
python
- Import and test the encoder by running these lines one at a time:
from encoder import base62_encode
base62_encode(1)
base62_encode(62)
base62_encode(1000000)
What should I see?
You should see these results:
- base62_encode(1) returns '1' (single digit stays the same).
- base62_encode(62) returns '10' (rolls over to two characters, just like decimal 10 rolls over from single digits).
- base62_encode(1000000) returns '4c92' (a million compressed into four characters).
- Exit the Python shell by typing:
exit()
Seeing a ModuleNotFoundError?
This means Python cannot find encoder.py. Make sure your terminal's current directory is the url-shortener folder (where encoder.py lives). Run dir to list files and confirm you can see encoder.py in the output.
help me debug why Python can't find my encoder module
Why Base62 instead of Base64 or hashing?
Base64 includes + and / characters that need special escaping inside URLs. Base62 uses only letters and digits, so every code is safe to paste directly into a URL with no encoding issues.
Hash-based approaches (like MD5 or SHA) require truncation to keep codes short, which introduces collision risk. You would need to check whether a code already exists before saving it. A counter-based approach with Base62 guarantees uniqueness because every number maps to exactly one code.
Your encoding module is complete and tested. Next up, you'll build the Flask API that uses this encoder to shorten real URLs and store them in Redis.
Create the Flask API with Redis Storage
Your Base62 encoder can turn any number into a compact short code. Now you need the web layer that ties everything together.
In this step, you will build two API endpoints with Flask. One accepts a long URL and returns a short code. The other takes a short code and redirects the user to the original URL. Redis handles all the storage and counter logic between them.
In this step, get ready to:
- Create app.py with Flask and a Redis connection.
- Build a POST endpoint that shortens URLs using atomic counters and Base62 encoding.
- Build a GET endpoint that redirects visitors and tracks clicks.
Create app.py with Flask and Redis
Every URL shortener needs two things: a way to accept URLs and a fast storage layer to look them up later. You will set up both in a single file.
- In VS Code, create a new file called app.py inside your url-shortener folder.
- Add the following code to import Flask, connect to Redis, and import your encoder:
from flask import Flask, request, redirect, jsonify
import redis
from encoder import base62_encode
app = Flask(__name__)
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
What does this code do?
- flask gives you routing and JSON handling for your API endpoints.
- redis.Redis(...) creates a connection to your local Redis server. The decode_responses=True setting means Redis returns regular strings instead of raw bytes.
- from encoder import base62_encode pulls in the encoder you built in the previous step.
You won't see output yet. The next chunk adds the shorten endpoint that puts this setup to work.
- Below the Redis connection line in app.py, add the POST /shorten endpoint:
@app.route("/shorten", methods=["POST"])
def shorten_url():
data = request.get_json()
if not data or "url" not in data:
return jsonify({"error": "Missing 'url' field"}), 400
original_url = data["url"]
# Validate URL has a scheme
if not original_url.startswith(("http://", "https://")):
original_url = "https://" + original_url
# Atomically increment counter to get a unique ID
url_id = r.incr("next_url_id")
# Encode the ID to a short code
short_code = base62_encode(url_id)
# Store the mapping in Redis
r.set(f"url:{short_code}", original_url)
# Set expiration if requested
expires_in = data.get("expires_in")
if expires_in:
r.expire(f"url:{short_code}", int(expires_in))
short_url = f"http://localhost:5000/{short_code}"
return jsonify({
"short_url": short_url,
"short_code": short_code,
"original_url": original_url
}), 201
What does this endpoint do?
This is the core of your URL shortener. Here is the flow:
- r.incr("next_url_id") atomically increments a counter in Redis and returns the new value. "Atomic" means no two requests can ever get the same number, even under heavy traffic. This guarantees unique IDs without collision checks.
- base62_encode(url_id) converts that numeric ID into a compact short code like 4c92.
- r.set(f"url:{short_code}", original_url) stores the short-code-to-URL mapping. Redis lookups are O(1), meaning redirect speed stays constant no matter how many URLs you store.
- The expires_in block is a hook for URL expiration. If the request includes a time-to-live value, Redis automatically deletes the key after that many seconds.
Why use Redis INCR instead of random IDs?
Random ID generation (like UUIDs or hashed values) requires checking whether the ID already exists before saving. Under high traffic, these collision checks become expensive.
An atomic counter sidesteps the problem entirely. Redis guarantees every INCR call returns a unique, sequential number. This is the same strategy production URL shorteners use for reliable ID generation at scale.
The trade-off is that sequential codes are predictable (someone could guess other short codes). For a production public shortener you would add randomization, but for learning system design the counter approach is the clearest demonstration of atomic operations.
You won't see a change yet. The redirect endpoint in the next section completes the app so you can test the full flow.
Build the redirect endpoint
The shorten endpoint writes data into Redis. Now you need the read side: when someone visits a short URL, your app looks up the original URL and sends them there.
- Below the shorten_url function in app.py, add the redirect endpoint and the app runner:
@app.route("/<short_code>")
def redirect_url(short_code):
original_url = r.get(f"url:{short_code}")
if original_url is None:
return jsonify({"error": "Short URL not found or expired"}), 404
# Increment click counter
r.incr(f"clicks:{short_code}")
return redirect(original_url, code=302)
if __name__ == "__main__":
app.run(debug=True)
What does this endpoint do?
- r.get(f"url:{short_code}") performs an O(1) lookup in Redis. This is the "read-heavy" side of your shortener. In production, redirects happen far more often than new URL creations.
- r.incr(f"clicks:{short_code}") tracks how many times each short URL has been visited. This is basic analytics.
- redirect(original_url, code=302) sends an HTTP 302 (temporary) redirect. A 302 means the browser asks your server every single time, so you can count every click. A 301 (permanent) would let browsers cache the redirect and skip your server entirely.
Why 302 instead of 301?
HTTP 301 tells the browser "this redirect is permanent, cache it." That means subsequent visits bypass your server completely. You would lose all click tracking.
HTTP 302 tells the browser "this redirect is temporary, always ask me." Every visit hits your server, so r.incr() records every click. This is the standard choice for analytics-aware URL shorteners.
- Save app.py.
Seeing a red underline on an import?
If VS Code shows a warning on from encoder import base62_encode, make sure encoder.py is in the same folder as app.py (both directly inside url-shortener). Also confirm your VS Code Python interpreter is set to the virtual environment (venv).
help me fix import errors in my Flask app
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py before moving on to testing.
ⓧ I'd like to double check the full code
Here is the complete app.py file at this point:
from flask import Flask, request, redirect, jsonify
import redis
from encoder import base62_encode
app = Flask(__name__)
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
@app.route("/shorten", methods=["POST"])
def shorten_url():
data = request.get_json()
if not data or "url" not in data:
return jsonify({"error": "Missing 'url' field"}), 400
original_url = data["url"]
# Validate URL has a scheme
if not original_url.startswith(("http://", "https://")):
original_url = "https://" + original_url
# Atomically increment counter to get a unique ID
url_id = r.incr("next_url_id")
# Encode the ID to a short code
short_code = base62_encode(url_id)
# Store the mapping in Redis
r.set(f"url:{short_code}", original_url)
# Set expiration if requested
expires_in = data.get("expires_in")
if expires_in:
r.expire(f"url:{short_code}", int(expires_in))
short_url = f"http://localhost:5000/{short_code}"
return jsonify({
"short_url": short_url,
"short_code": short_code,
"original_url": original_url
}), 201
@app.route("/<short_code>")
def redirect_url(short_code):
original_url = r.get(f"url:{short_code}")
if original_url is None:
return jsonify({"error": "Short URL not found or expired"}), 404
# Increment click counter
r.incr(f"clicks:{short_code}")
return redirect(original_url, code=302)
if __name__ == "__main__":
app.run(debug=True)
Run and test your URL shortener
Your code is written. Time to start the server and prove it works end-to-end.
- Make sure your virtual environment is still active (you should see (venv) at the start of your Command Prompt line).
- Start the Flask development server by running:
flask run
What should I see?
Flask prints a few lines including Running on http://127.0.0.1:5000. This means your server is listening for requests. Keep this terminal window open.
Seeing a ModuleNotFoundError?
If you see an error mentioning flask or redis not found, your virtual environment is not active. Run venv\Scripts\activate first, then try flask run again.
If the error mentions encoder, make sure you are running the command from inside the url-shortener folder (where both app.py and encoder.py live).
help me fix ModuleNotFoundError when running flask
- Open a second Command Prompt window (keep the Flask server running in the first one).
- Test the shorten endpoint by running this curl command:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d "{\"url\": \"https://www.example.com\"}"
What should I see?
You should see a JSON response like:
{
"original_url": "https://www.example.com",
"short_code": "1",
"short_url": "http://localhost:5000/1"
}
The short_code is 1 because this is the first URL you have shortened (counter starts at 1). Each new URL gets the next number, encoded in Base62.
- Copy the short_url value from the response (e.g. http://localhost:5000/1) and paste it into your browser's address bar.
Your browser should redirect to https://www.example.com. That redirect proves both endpoints are working together: the POST created the mapping, and the GET looked it up and sent you to the original URL.
Browser shows a 404 JSON error?
If you see {"error": "Short URL not found or expired"}, Redis may not be running. Open a new Command Prompt and run C:\redis\redis-cli.exe ping. If you do not get PONG back, restart your Redis server in its own terminal window.
help me debug why my short URL redirect returns 404
Your shortener is live. You can create short URLs and redirect visitors. Next up, you will add a stats endpoint so you can see how many clicks each short URL receives.
Add a Stats Endpoint and Test the Full Flow
Your shortener can create short URLs and redirect visitors. Now you need a way to see how those links are performing.
In this step, you will add a stats endpoint that reports click counts for any short code. Then you will run through the entire flow end-to-end to prove everything works together.
In this step, get ready to:
- Add a stats endpoint that returns click counts and the original URL.
- Test the full shorten-redirect-stats flow with multiple URLs.
- Connect your implementation to real system design interview concepts.
Add the stats endpoint to your API
URL shorteners are read-heavy systems. Most of the traffic is redirects, but operators also need visibility into how links perform. A stats endpoint gives you that visibility by exposing click counts per short code.
- In VS Code, open app.py and scroll down to the if __name__ == "__main__": block at the bottom.
- Add the following stats endpoint above that block, directly after the redirect_url function:
@app.route("/stats/<short_code>")
def get_stats(short_code):
original_url = r.get(f"url:{short_code}")
if original_url is None:
return jsonify({"error": "Short URL not found or expired"}), 404
clicks = r.get(f"clicks:{short_code}")
ttl = r.ttl(f"url:{short_code}")
stats = {
"short_code": short_code,
"original_url": original_url,
"clicks": int(clicks) if clicks else 0,
}
if ttl >= 0:
stats["expires_in_seconds"] = ttl
return jsonify(stats)
What does this code do?
- r.get(f"url:{short_code}") looks up the original URL. If the key does not exist (expired or never created), the endpoint returns a 404.
- r.get(f"clicks:{short_code}") fetches the current click count. The redirect endpoint increments this value every time someone visits the short URL.
- r.ttl(f"url:{short_code}") checks how many seconds remain before the key expires. If no TTL is set, Redis returns -1, so the if ttl >= 0 guard only includes expiration info when a TTL is active.
- Save app.py.
- In the Command Prompt window where Flask is running, press Ctrl+C to stop the server.
- Restart the server by running:
flask run
- You should see the * Running on http://127.0.0.1:5000 message appear again.
Server won't start?
- Check the error message in your terminal. A SyntaxError usually means a missing colon or incorrect indentation in the new function.
- Make sure the new get_stats function is at the same indentation level as shorten_url and redirect_url (no extra indentation).
- Confirm your virtual environment is still active. You should see (venv) at the start of your command prompt.
Why won't my Flask server restart?
Test the full shorten-redirect-stats flow
Now you will exercise the entire system end-to-end. You will shorten a couple of URLs, visit them to generate clicks, then check the stats endpoint to confirm everything is connected.
- Open a new Command Prompt window (keep the Flask server running in the other one).
- Shorten your first URL by running this command:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d "{\"url\": \"https://www.wikipedia.org\"}"
- You should see a JSON response containing a short_url, short_code, and original_url field.
- Shorten a second URL by running:
curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" -d "{\"url\": \"https://github.com\"}"
What short codes should I see?
If this is a fresh Redis database, your first short code will be 1 and the second will be 2. If you shortened URLs in the previous step while testing, your counter will be higher. That is expected.
- Open your browser and visit the first short URL from the response (for example, http://localhost:5000/1). You should be redirected to Wikipedia.
- Visit the same short URL two more times to generate additional clicks.
- Now check the stats for that short code by running this command (replace 1 with your actual short code):
curl http://localhost:5000/stats/1
- You should see a JSON response showing the original_url, short_code, and a clicks count of 3 (or however many times you visited the short URL).
Seeing 0 clicks?
- Make sure you visited the short URL in your browser, not just the stats endpoint. The click counter only increments on the redirect endpoint.
- Check that the short code in your stats request matches the short code returned by the shorten endpoint. They are case-sensitive.
Why is my click count not incrementing?
Connect your implementation to system design concepts
You now have a working URL shortener that demonstrates the core patterns interviewers expect you to explain. Let's map each piece of your code to the system design decision it represents.
How does this map to a system design interview?
- Collision-free ID generation. Redis INCR atomically increments a counter, guaranteeing every URL gets a unique ID without coordination or collision checks.
- Compact encoding. Base62 converts numeric IDs into short alphanumeric strings. Six characters give you approximately 56.8 billion unique codes.
- O(1) redirect lookups. Redis GET retrieves the original URL in sub-millisecond time. This matters because the redirect path is the hot path with the most traffic.
- 302 redirect for analytics. A 302 (temporary) redirect forces the browser to hit your server on every visit. A 301 (permanent) would let browsers cache the redirect, bypassing your click counter entirely.
These are the exact trade-offs that system design discussions cover. You can now explain each decision from hands-on experience, not just theory.
Why Redis over a SQL database?
URL shorteners are read-heavy. For every URL you shorten (one write), you may serve thousands of redirects (many reads). Redis stores everything in memory, giving you sub-millisecond reads without the overhead of parsing SQL queries or managing table indexes.
A SQL database would work functionally, but Redis is what production shorteners actually use because the access pattern (key in, value out) maps perfectly to a key-value store.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and that your Flask server is running.
ⓧ I'd like to double check the full code
Here is the complete app.py file with all three endpoints:
from flask import Flask, request, redirect, jsonify
import redis
from encoder import base62_encode
app = Flask(__name__)
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
@app.route("/shorten", methods=["POST"])
def shorten_url():
data = request.get_json()
if not data or "url" not in data:
return jsonify({"error": "Missing 'url' field"}), 400
original_url = data["url"]
# Validate URL has a scheme
if not original_url.startswith(("http://", "https://")):
original_url = "https://" + original_url
# Atomically increment counter to get a unique ID
url_id = r.incr("next_url_id")
# Encode the ID to a short code
short_code = base62_encode(url_id)
# Store the mapping in Redis
r.set(f"url:{short_code}", original_url)
# Set expiration if requested
expires_in = data.get("expires_in")
if expires_in:
r.expire(f"url:{short_code}", int(expires_in))
short_url = f"http://localhost:5000/{short_code}"
return jsonify({
"short_url": short_url,
"short_code": short_code,
"original_url": original_url
}), 201
@app.route("/<short_code>")
def redirect_url(short_code):
original_url = r.get(f"url:{short_code}")
if original_url is None:
return jsonify({"error": "Short URL not found or expired"}), 404
# Increment click counter
r.incr(f"clicks:{short_code}")
return redirect(original_url, code=302)
@app.route("/stats/<short_code>")
def get_stats(short_code):
original_url = r.get(f"url:{short_code}")
if original_url is None:
return jsonify({"error": "Short URL not found or expired"}), 404
clicks = r.get(f"clicks:{short_code}")
ttl = r.ttl(f"url:{short_code}")
stats = {
"short_code": short_code,
"original_url": original_url,
"clicks": int(clicks) if clicks else 0,
}
if ttl >= 0:
stats["expires_in_seconds"] = ttl
return jsonify(stats)
if __name__ == "__main__":
app.run(debug=True)
Your URL shortener is fully functional. You have built a REST API that shortens URLs, redirects visitors, and tracks clicks. Up next, the Secret Mission challenges you to add automatic link expiration using Redis TTL.
Secret mission
Add URL Expiration with Redis TTL
Real URL shorteners don't keep links alive forever. Marketing campaigns end, temporary shares expire, and storage needs managing. Can you add automatic link expiration so short URLs self-destruct after a configurable time period?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Redis server running at C:\redis.
- The url-shortener project folder on your Desktop (includes virtual environment and installed packages).
- Redis data stored in memory (cleared when the server stops).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your URL shortener.
- Your Redis server continues running in its Command Prompt window.
- Your project folder and virtual environment stay on your Desktop.
- Redis for Windows remains installed at C:\redis.
Pause - I'll come back to this later
Shut down the Redis server to free up memory. Your project files stay on disk so you can pick up where you left off.
- Go to the Command Prompt window where Redis is running.
- Press Ctrl+C to stop the Redis server.
- Close that Command Prompt window.
- Your project folder and virtual environment remain on your Desktop for next time.
Resuming later
To start again, open a Command Prompt, run C:\redis\RedisService.exe run --foreground, then activate your virtual environment and run flask run in the project folder. Any previously shortened URLs will be gone because Redis stores data in memory.
Delete - I don't want to use this again
Remove all project resources and Redis from your machine to start completely fresh.
- Stop the Redis server by pressing Ctrl+C in its Command Prompt window.
- Delete the Redis installation by running this in a new Command Prompt:
rmdir /s /q C:\redis
- Delete the project folder by running:
rmdir /s /q %USERPROFILE%\Desktop\url-shortener
- That removes your virtual environment, installed packages, and all project code.
That's a Wrap!
That's a Wrap!
Nice work. You just built a fully functional URL shortener from scratch and learned system design by doing it, not just reading about it.
You've learned how to:
- Build a REST API with Flask that shortens URLs, redirects users with 302 redirects, and tracks clicks using Redis INCR.
- Implement Base62 encoding to convert atomic counter IDs into compact, URL-safe short codes without collision checks.
- Use Redis as a high-performance key-value store with O(1) lookups for the redirect hot path and atomic counter operations for collision-free ID generation.
- Secret Mission: Added TTL-based URL expiration using Redis EXPIRE so short links automatically clean themselves up.
Ready to quiz yourself?