Build a URL Shortener with Flask
Build a URL shortener web app with Flask, SQLite, and a styled frontend.
Introduction
30 Second Summary
Think about how many links you share every day. Some URLs are so long they break in emails, look ugly in messages, and are impossible to remember.
In this project, you will build your own URL shortener using Python and Flask. Paste in any long URL, get back a short link, and click it to get redirected to the original page.
What You'll Build
A local web application that generates short codes for long URLs, stores them in a SQLite database, and redirects visitors to the original links.
By the end of this project, you'll have:
- A Flask web server with routes for shortening URLs and handling redirects.
- A SQLite database that stores URL mappings and persists between server restarts.
- A styled HTML page with a form for submitting URLs and displaying shortened links.
- Secret Mission: Add click tracking and a stats dashboard to monitor how many times each short link gets visited.
Are there any prerequisites?
None. This guide walks you through every step, from setting up your project folder to running the finished app. All you need is a computer with Python and VS Code installed.
Before We Start
Before We Start...
Set Up Your Project and Install Flask
Building a web application starts with a clean workspace. You'll create a dedicated project folder, set up an isolated Python environment, and install Flask.
By the end of this step, you'll have a live web server running on your machine. It will respond with "Hello, World!" in your browser.
In this step, get ready to:
- Create the url-shortener project directory on your Desktop.
- Set up and activate a Python virtual environment.
- Install Flask and confirm it works with a "Hello, World!" app.
Create your project directory and virtual environment
- 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
Seeing 'directory already exists'?
If you already have a url-shortener folder on your Desktop from a previous attempt, delete it first or choose a different name.
help me fix this mkdir error
Your project needs its own isolated Python environment so that packages you install here don't interfere with other projects on your machine.
What is a virtual environment?
A virtual environment is an isolated Python workspace for your project. It has its own copy of Python and its own set of installed packages.
On modern macOS, using a virtual environment is required. Without one, pip install commands will fail with an "externally managed environment" error.
- Create and activate a virtual environment by running these commands:
macOS
python3 -m venv .venv
source .venv/bin/activate
Windows
python -m venv .venv
.venv\Scripts\activate
What do these commands do?
- python3 -m venv .venv creates a hidden folder called .venv containing an isolated Python installation and its own pip.
- source .venv/bin/activate switches your terminal session to use this isolated Python instead of the system one.
- Check your terminal prompt. You should see (.venv) at the beginning of the line.
Don't see (.venv) in your prompt?
- Make sure you ran the activate command, not just the venv creation command. The two steps are separate.
- On Windows, make sure you ran .venv\Scripts\activate (backslashes, not forward slashes).
- If you see a permissions error on Windows, try running PowerShell as administrator and executing Set-ExecutionPolicy RemoteSigned first.
Help me fix my virtual environment activation
Install Flask
- Install Flask inside your virtual environment by running this command:
pip install flask==3.1.3
What is Flask?
Flask is a lightweight Python web framework. It handles the plumbing of receiving web requests and sending back responses, so you can focus on your application's logic.
The ==3.1.3 pins the exact version. This ensures your setup matches this project's instructions.
- Verify Flask installed correctly by running this command:
python -c "import flask; print(flask.__version__)"
You should see 3.1.3 printed in the terminal.
Seeing an error instead of a version number?
- Check that (.venv) still appears in your terminal prompt. If not, re-run the activate command from the previous substep.
- Make sure you typed pip install flask==3.1.3 with two equals signs, not one.
- If you see a network error, check your internet connection and try the install command again.
Help me troubleshoot my Flask installation
Create a minimal Flask app and run it
Flask is installed, but it needs a Python file to know what your app does. You'll create a single file called app.py with the simplest possible web page.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the url-shortener folder, then click Open.
Why app.py?
Flask automatically looks for a file named app.py when you run the flask run command. Naming your file this way means you don't need any extra configuration flags.
- In the Explorer sidebar on the left, hover over the URL-SHORTENER heading.
- Click the New File icon (it looks like a page with a plus sign).
- Type app.py and press Enter.
- Copy the following code into app.py:
from flask import Flask
# Create the Flask application instance
app = Flask(__name__)
# Define the home page route
@app.route('/')
def index():
return 'Hello, World!'
# Run the app with debug mode when executed directly
if __name__ == '__main__':
app.run(debug=True)
What does this code do?
- from flask import Flask brings in the Flask class from the library you just installed.
- app = Flask(__name__) creates a new Flask application. The __name__ tells Flask where to find your project files.
- @app.route('/') is a decorator that tells Flask to call the index() function when someone visits the home page.
- if __name__ == '__main__': runs the server when you execute the file directly with python app.py.
- Save app.py by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Time to see your app in action. You'll start the Flask development server and open the page in your browser.
- Switch back to your Terminal window (the one that still has (.venv) in the prompt).
- Start the Flask development server by running this command:
flask run --debug
What does --debug do?
The --debug flag enables two features. The server automatically restarts whenever you save a code change. It also shows detailed error messages in the browser if something breaks.
You should see output including Running on http://127.0.0.1:5000. This means your server is running.
Your terminal is now busy
The Flask server keeps running in this terminal window. Leave it open for the rest of the project. When you need to run other commands later, you'll open a new terminal tab or window.
- Open a new tab in your web browser.
- Paste http://127.0.0.1:5000 into the address bar and press Enter.
You should see "Hello, World!" displayed as plain text in the browser.
Don't see Hello, World!?
- Check that the Flask server is still running in your terminal. If it stopped, run flask run --debug again.
- Make sure you're visiting http://127.0.0.1:5000 and not https:// (no 's').
- Verify that your app.py file is saved. Unsaved changes won't be picked up by the server.
Help me fix my Flask server
✔️ Awesome, I've got everything!
Great work. Make sure you've saved app.py and keep your Flask server running in the terminal.
ⓧ I'd like to double check the full code
Here is the full app.py file. Compare it with yours and make sure everything matches.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
Your development environment is ready and your first Flask route is live. Next up, you'll build the database layer that stores URL mappings.
Create the Database Layer
Your Flask server is up and running. Right now it can only say "Hello, World!" because it has nowhere to store data.
Every URL you shorten needs to be saved somewhere so the app can look it up later and redirect visitors. In this step, you'll create a SQLite database to store those URL mappings.
In this step, get ready to:
- Write a database connection function that your Flask app uses to talk to SQLite.
- Define a table schema that stores original URLs, short codes, and timestamps.
- Initialize the database automatically when the server starts.
Add the database connection function
SQLite is a lightweight database that stores everything in a single file. Python has a built-in sqlite3 module for working with it, so there's nothing extra to install.
- In app.py, find the from flask import Flask line at the top. Add this import directly below it:
import sqlite3
- Next, find the line app = Flask(__name__). Copy the following code below it:
DATABASE = 'urls.db'
def get_db_connection():
# Connect to the SQLite database file
conn = sqlite3.connect(DATABASE)
# Allow accessing columns by name instead of index
conn.row_factory = sqlite3.Row
return conn
What does this code do?
- DATABASE = 'urls.db' stores the database filename in one place. If you ever rename the file, you only update this one line.
- sqlite3.connect(DATABASE) opens a connection to the database file. If the file does not exist yet, SQLite creates it automatically.
- conn.row_factory = sqlite3.Row lets you access query results by column name (like row['original_url']) instead of by number index.
You won't see any changes in the browser yet. The next function defines the table structure that holds your URLs.
- Still in app.py, add the following function directly below get_db_connection():
def init_db():
conn = get_db_connection()
# Create the urls table if it doesn't already exist
conn.execute('''
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_url TEXT NOT NULL,
short_code TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Save the changes and close the connection
conn.commit()
conn.close()
What does this code do?
- CREATE TABLE IF NOT EXISTS creates the table only when it does not already exist. This means you can run the function multiple times safely without errors.
- Each column has a type and constraints. NOT NULL means the column cannot be empty. UNIQUE on short_code prevents duplicate short codes from being stored.
- CURRENT_TIMESTAMP automatically records the date and time when each URL is shortened.
- conn.commit() saves the table creation to disk. conn.close() releases the connection so it does not stay open.
- Save app.py.
Both functions are defined but not called yet. In the next substep, you'll wire up init_db() so the database is created automatically when the server starts.
Initialize the database on server start
The init_db() function exists, but nothing calls it yet. You need to run it once before the server starts accepting requests.
- In app.py, scroll to the bottom and find the if __name__ == '__main__': block. It currently looks like this:
if __name__ == '__main__':
app.run(debug=True)
- Add init_db() on the line before app.run(debug=True), so the block now looks like this:
if __name__ == '__main__':
init_db()
app.run(debug=True)
Why place init_db() here?
The if __name__ == '__main__': block only runs when you execute the file directly with python app.py. Placing init_db() before app.run() ensures the database table exists before the server starts accepting requests.
- Save app.py.
- In your terminal, stop the running server by pressing Ctrl+C.
- Start the server using python app.py by running this command:
python app.py
Why python app.py instead of flask run?
The flask run command imports your file as a module, which skips the if __name__ == '__main__': block entirely. Running python app.py executes the file directly, which triggers init_db() and creates the database.
You still get debug mode because app.run(debug=True) enables it directly in the code.
- Check the Explorer sidebar in VS Code. You should see urls.db listed in your url-shortener folder alongside app.py.
Don't see urls.db?
- Make sure you ran python app.py (not flask run). The flask run command does not execute the if __name__ == '__main__': block.
- Check that init_db() is indented inside the if __name__ block and appears before app.run(debug=True).
- Confirm your virtual environment is still active. You should see (.venv) at the start of your terminal prompt.
Still stuck? Ask for help.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py file at this point. Compare it with yours to spot any differences.
from flask import Flask
import sqlite3
app = Flask(__name__)
DATABASE = 'urls.db'
def get_db_connection():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
conn.execute('''
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_url TEXT NOT NULL,
short_code TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
@app.route('/')
def index():
return 'Hello, World!'
if __name__ == '__main__':
init_db()
app.run(debug=True)
Your database is ready and waiting for URLs. Next up, you'll build the code that generates short codes and saves URL mappings.
Build the URL Shortening Feature
Your database is set up and ready to store URL mappings. Now it's time to build the core feature of your app: turning a long URL into a short code and saving the pair to your database.
You'll write a function that generates random 6-character codes, create an HTML form where users can submit URLs, and update your home route to handle the full shortening flow.
In this step, get ready to:
- Write a function that generates unique 6-character short codes.
- Create an HTML template with a form for submitting URLs.
- Update the home route to shorten URLs and display the result.
Add a short code generator function
Every shortened URL needs a unique code like aB3xKq. Your generator will pick 6 random characters from letters and digits, then check the database to make sure that code isn't already taken.
- In app.py, find the line that says import sqlite3.
- Add these two new imports directly below it:
import string
import random
- Now scroll down to the init_db() function. Below its closing conn.close() line, add the short code generator by pasting this function:
def generate_short_code(length=6):
# Build a pool of letters and digits (62 possible characters)
characters = string.ascii_letters + string.digits
conn = get_db_connection()
while True:
# Generate a random 6-character code
code = ''.join(random.choices(characters, k=length))
# Check if this code already exists in the database
existing = conn.execute(
'SELECT id FROM urls WHERE short_code = ?', (code,)
).fetchone()
# If the code is unique, return it
if not existing:
conn.close()
return code
What does this code do?
- string.ascii_letters + string.digits creates a pool of 62 characters (a-z, A-Z, 0-9). With 6 characters, that gives over 56 billion possible codes.
- random.choices(characters, k=length) picks 6 random characters from the pool.
- The while True loop keeps generating codes until it finds one that doesn't already exist in the database. This prevents collisions.
- Save app.py.
- Check the terminal where your Flask server is running. It should auto-reload and display * Restarting with stat with no error messages.
Seeing an ImportError or SyntaxError?
- Make sure import string and import random are on their own separate lines, directly below import sqlite3.
- Check that the generate_short_code function is indented correctly. The def keyword should have no indentation, and the function body should be indented one level.
- Help me fix my generate_short_code function in app.py
Create the HTML template
Flask uses a templating engine called Jinja to render HTML pages with dynamic data. It looks for template files inside a folder called templates.
- In VS Code, right-click in the Explorer sidebar (where your files are listed).
- Select New Folder and name it templates.
- Right-click on the templates folder and select New File.
- Name the file index.html.
- Confirm the file was created by running this in a new terminal:
ls templates
You should see index.html in the output.
- Open templates/index.html in VS Code and paste the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Shortener</title>
</head>
<body>
<div class="container">
<h1>URL Shortener</h1>
<p>Paste a long URL below to get a short link.</p>
<!-- Form submits a POST request to the same page -->
<form method="POST">
<input type="text" name="original_url" placeholder="https://example.com/very/long/url" required>
<button type="submit">Shorten</button>
</form>
<!-- Only show the result section when a short URL exists -->
{% if short_url %}
<div class="result">
<p>Your shortened URL:</p>
<a href="{{ short_url }}" target="_blank">{{ short_url }}</a>
</div>
{% endif %}
</div>
</body>
</html>
What does this template do?
- The <form method="POST"> creates a form that sends the submitted URL back to the same page as a POST request.
- The name="original_url" attribute on the input field is how Flask identifies the submitted value on the server side.
- {% if short_url %} is Jinja syntax. It only renders the result section when the server passes a short_url value to the template.
- Save templates/index.html.
You won't see changes in the browser yet. The next substep updates the route to use this template.
✔️ 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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Shortener</title>
</head>
<body>
<div class="container">
<h1>URL Shortener</h1>
<p>Paste a long URL below to get a short link.</p>
<form method="POST">
<input type="text" name="original_url" placeholder="https://example.com/very/long/url" required>
<button type="submit">Shorten</button>
</form>
{% if short_url %}
<div class="result">
<p>Your shortened URL:</p>
<a href="{{ short_url }}" target="_blank">{{ short_url }}</a>
</div>
{% endif %}
</div>
</body>
</html>
Update the index route to shorten URLs
Right now, your home route just returns plain text. You need to update it to render the HTML template, accept form submissions, and save shortened URLs to the database.
- In app.py, find this line at the very top of the file:
from flask import Flask
- Replace it with this updated import that adds render_template, request, and redirect:
from flask import Flask, render_template, request, redirect
What are these new imports?
- render_template loads an HTML file from the templates folder and fills in any dynamic values.
- request gives you access to incoming form data from the browser.
- redirect sends the user to a different URL. You'll use this in the next step.
- Now scroll down to the current index route. It looks like this:
@app.route('/')
def index():
return 'Hello, World!'
- Replace the entire route (all three lines) with this updated version:
@app.route('/', methods=['GET', 'POST'])
def index():
short_url = None
if request.method == 'POST':
# Get the URL from the form and clean up whitespace
original_url = request.form.get('original_url', '').strip()
if original_url:
# Auto-add https:// if the user didn't include a scheme
if not original_url.startswith(('http://', 'https://')):
original_url = 'https://' + original_url
# Generate a unique short code and save the mapping
short_code = generate_short_code()
conn = get_db_connection()
conn.execute(
'INSERT INTO urls (original_url, short_code) VALUES (?, ?)',
(original_url, short_code)
)
conn.commit()
conn.close()
# Build the full shortened URL
short_url = request.host_url + short_code
return render_template('index.html', short_url=short_url)
What does this route do?
- methods=['GET', 'POST'] tells Flask this route handles both regular page loads (GET) and form submissions (POST).
- On a GET request, short_url stays None, so the template renders just the form.
- On a POST request, the route reads the submitted URL, auto-prefixes https:// if no scheme was provided, generates a short code, and saves it to the database.
- request.host_url returns http://127.0.0.1:5000/, so the final shortened URL looks like http://127.0.0.1:5000/aB3xKq.
Why auto-prefix https://?
If a user types example.com without https://, Flask's redirect() would treat it as a relative path on your server instead of an external URL. Adding the scheme ensures the redirect goes to the right place.
- Save app.py.
- Open your browser and go to http://127.0.0.1:5000. You should see a form with a text input and a Shorten button.
- Type https://www.google.com/search?q=url+shortener into the input field and click Shorten.
A shortened URL should appear below the form, something like http://127.0.0.1:5000/aB3xKq. The 6-character code at the end is randomly generated, so yours will be different.
Not seeing the form?
- Check the terminal for errors. A TemplateNotFound error means Flask can't find index.html. Make sure your folder is named exactly templates (not template) and the file inside is named index.html.
- If the page still shows Hello, World!, double check that you replaced the old index route with the new one. The route should start with @app.route('/', methods=['GET', 'POST']).
- If clicking Shorten gives you a Method Not Allowed error, make sure methods=['GET', 'POST'] is present in the route decorator.
- Help me debug my Flask URL shortener form
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
from flask import Flask, render_template, request, redirect
import sqlite3
import string
import random
app = Flask(__name__)
DATABASE = 'urls.db'
def get_db_connection():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
conn.execute('''
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_url TEXT NOT NULL,
short_code TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def generate_short_code(length=6):
characters = string.ascii_letters + string.digits
conn = get_db_connection()
while True:
code = ''.join(random.choices(characters, k=length))
existing = conn.execute(
'SELECT id FROM urls WHERE short_code = ?', (code,)
).fetchone()
if not existing:
conn.close()
return code
@app.route('/', methods=['GET', 'POST'])
def index():
short_url = None
if request.method == 'POST':
original_url = request.form.get('original_url', '').strip()
if original_url:
if not original_url.startswith(('http://', 'https://')):
original_url = 'https://' + original_url
short_code = generate_short_code()
conn = get_db_connection()
conn.execute(
'INSERT INTO urls (original_url, short_code) VALUES (?, ?)',
(original_url, short_code)
)
conn.commit()
conn.close()
short_url = request.host_url + short_code
return render_template('index.html', short_url=short_url)
if __name__ == '__main__':
init_db()
app.run(debug=True)
Your URL shortener can now accept long URLs and generate short codes for them. Next up, you'll make those short links actually work by adding a redirect route that sends visitors to the original URL.
Add the Redirect Route
Your URL shortener can accept a long URL and generate a short code for it. The short link even shows up on the page after you submit the form.
There is one problem. If you click that short link right now, nothing happens. Flask does not know what to do when someone visits /aB3xKq because there is no route handling those URLs yet. In this step, you will build the redirect route that makes the whole app work.
In this step, get ready to:
- Add a redirect route that looks up short codes in the database and sends users to the original URL.
- Test the complete end-to-end flow from shortening a URL to being redirected.
Add a redirect route to app.py
Flask lets you capture parts of a URL as function parameters using angle brackets. A route like /<short_code> will match any path after the slash and pass that value into your function as an argument.
- In app.py, find the if __name__ == '__main__': block near the bottom of the file.
- Add the following route above that block, leaving a blank line between the end of the index() function and this new route:
@app.route('/<short_code>')
def redirect_url(short_code):
# Connect to the database and look up the short code
conn = get_db_connection()
url_data = conn.execute(
'SELECT original_url FROM urls WHERE short_code = ?', (short_code,)
).fetchone()
conn.close()
# Redirect to the original URL, or return 404 if not found
if url_data:
return redirect(url_data['original_url'])
return 'URL not found', 404
What does this code do?
The /<short_code> route captures whatever comes after the slash in the URL. So visiting /aB3xKq passes "aB3xKq" into the function as short_code.
The function queries the urls table for a matching record. If it finds one, redirect() sends the browser to the original URL with an HTTP redirect response. If no match exists, it returns a 404 error.
- Save app.py.
- Your Flask server should automatically restart in the terminal because you are running in debug mode. Look for a line like Restarting with stat in the terminal output.
Server not restarting?
If the server did not auto-restart, press Ctrl+C in the terminal to stop it, then start it again by running flask run --debug. Make sure your virtual environment is still activated (you should see (.venv) at the start of your terminal prompt).
If you see a syntax error, double check that the new route is outside the index() function and not accidentally indented inside it.
Help me debug my Flask server restart issue
Test the complete flow
The redirect route is in place. Time to test the entire journey from pasting a long URL to being redirected through a short one.
- Open your browser and go to http://127.0.0.1:5000.
- Type a URL into the form, such as https://www.wikipedia.org, and click Shorten.
- You should see a shortened URL appear below the form, like http://127.0.0.1:5000/aB3xKq (your code will be different).
- Click the shortened link, or copy it and paste it into a new browser tab.
Your browser should redirect you to the original URL you submitted. If you used Wikipedia, you should land on the Wikipedia homepage.
- Try submitting a URL without https:// in front (for example, just github.com) and test that the redirect still works.
- Try visiting a fake short code like http://127.0.0.1:5000/fakecode and confirm you see URL not found in the browser.
Redirect not working?
If clicking the short link shows URL not found instead of redirecting, the short code might not be in the database. Delete the urls.db file, restart the server, and try shortening a new URL from the form.
If the page just reloads the form without redirecting, make sure you are pasting the short link into a new browser tab. Clicking it on the same page should also work because of the target="_blank" attribute on the link.
Help me debug my URL shortener redirect
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and your server is still running.
ⓧ I'd like to double check the full code
Here is the complete app.py file as it should look at the end of this step.
from flask import Flask, render_template, request, redirect
import sqlite3
import string
import random
app = Flask(__name__)
DATABASE = 'urls.db'
def get_db_connection():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db_connection()
conn.execute('''
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
original_url TEXT NOT NULL,
short_code TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def generate_short_code(length=6):
characters = string.ascii_letters + string.digits
conn = get_db_connection()
while True:
code = ''.join(random.choices(characters, k=length))
existing = conn.execute(
'SELECT id FROM urls WHERE short_code = ?', (code,)
).fetchone()
if not existing:
conn.close()
return code
@app.route('/', methods=['GET', 'POST'])
def index():
short_url = None
if request.method == 'POST':
original_url = request.form.get('original_url', '').strip()
if original_url:
if not original_url.startswith(('http://', 'https://')):
original_url = 'https://' + original_url
short_code = generate_short_code()
conn = get_db_connection()
conn.execute(
'INSERT INTO urls (original_url, short_code) VALUES (?, ?)',
(original_url, short_code)
)
conn.commit()
conn.close()
short_url = request.host_url + short_code
return render_template('index.html', short_url=short_url)
@app.route('/<short_code>')
def redirect_url(short_code):
conn = get_db_connection()
url_data = conn.execute(
'SELECT original_url FROM urls WHERE short_code = ?', (short_code,)
).fetchone()
conn.close()
if url_data:
return redirect(url_data['original_url'])
return 'URL not found', 404
if __name__ == '__main__':
init_db()
app.run(debug=True)
Your URL shortener is fully functional. You can shorten any URL and get redirected through a short link. Next up, you will give the app a proper visual makeover with CSS styling.
Style Your URL Shortener
Your URL shortener works end-to-end. You can submit a URL, get a short link, and click it to redirect to the original.
Right now, the page is plain unstyled HTML. In this step, you'll add a CSS stylesheet to give your app a clean, centered card layout with polished form controls.
In this step, get ready to:
- Create a CSS stylesheet with layout, form, and result styles.
- Link the stylesheet to your HTML template and verify the styled interface.
Create the static directory and stylesheet
Flask serves static files like CSS and images from a folder called static/. You need to create this folder and a stylesheet inside it.
- In your terminal (make sure you're inside the url-shortener folder), create the static/ directory and style.css file by running these commands:
mkdir static
touch static/style.css
- Confirm the file was created by running:
ls static
You should see style.css in the output.
Don't see style.css?
Make sure you ran the commands from inside the url-shortener folder, not from your Desktop or home directory.
Run pwd to check your current location. It should end with /url-shortener.
Help me figure out why my static directory wasn't created.
- Open static/style.css in VS Code and add the page layout styles:
/* Reset default browser spacing */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Center everything vertically and horizontally */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* White card in the center of the page */
.container {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
text-align: center;
}
/* Page heading */
h1 {
color: #1a1a2e;
margin-bottom: 0.5rem;
}
/* Subtitle text */
p {
color: #666;
margin-bottom: 1.5rem;
}
/* Place the input and button side by side */
form {
display: flex;
gap: 0.5rem;
}
What does this CSS do?
- The * reset removes default browser spacing so every element starts from a clean slate.
- The body uses flexbox to center the card both horizontally and vertically on a light gray background.
- .container creates a white card with rounded corners and a subtle shadow. It caps at 500px wide so it doesn't stretch across the whole screen.
- The form uses flexbox to place the URL input and the Shorten button side by side.
You won't see any changes in the browser yet. You'll link this stylesheet to your HTML in the next substep.
- Below the form styles in style.css, add the form control and result display styles:
/* URL input field */
input[type="text"] {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
/* Highlight the input border when focused */
input[type="text"]:focus {
border-color: #4361ee;
}
/* Shorten button */
button {
padding: 0.75rem 1.5rem;
background-color: #4361ee;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
/* Darken button on hover */
button:hover {
background-color: #3a56d4;
}
/* Box that shows the shortened URL */
.result {
margin-top: 1.5rem;
padding: 1rem;
background-color: #f0f7ff;
border-radius: 8px;
border: 1px solid #d0e3ff;
}
/* Label text inside the result box */
.result p {
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
}
/* The shortened URL link */
.result a {
color: #4361ee;
font-size: 1.1rem;
word-break: break-all;
}
What does this CSS do?
- input[type="text"] styles the URL text input with padding, rounded corners, and a blue border that appears when you click into it.
- button gives the Shorten button a blue background with white text. The :hover state darkens it slightly when the learner moves their cursor over it.
- .result creates a light blue box for the shortened URL output. The word-break: break-all on the link prevents long URLs from overflowing the card.
- Save style.css.
These styles still won't appear in the browser until you link the stylesheet to your HTML template.
✔️ Awesome, I've got everything!
Make sure you've saved style.css before moving on.
ⓧ I'd like to double check the full code
Here's the complete static/style.css file. Compare it against yours and make sure everything matches.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
text-align: center;
}
h1 {
color: #1a1a2e;
margin-bottom: 0.5rem;
}
p {
color: #666;
margin-bottom: 1.5rem;
}
form {
display: flex;
gap: 0.5rem;
}
input[type="text"] {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
input[type="text"]:focus {
border-color: #4361ee;
}
button {
padding: 0.75rem 1.5rem;
background-color: #4361ee;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #3a56d4;
}
.result {
margin-top: 1.5rem;
padding: 1rem;
background-color: #f0f7ff;
border-radius: 8px;
border: 1px solid #d0e3ff;
}
.result p {
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
}
.result a {
color: #4361ee;
font-size: 1.1rem;
word-break: break-all;
}
Link the stylesheet to your HTML
Flask uses a helper called url_for() to generate paths to static files. You'll add a <link> tag to your HTML that points to the stylesheet using this helper.
- In templates/index.html, find this line in the <head> section:
<title>URL Shortener</title>
- Add this line directly below it:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
What does url_for do here?
url_for('static', filename='style.css') tells Flask to generate the correct URL path to your style.css file inside the static/ folder.
Using url_for() is better than hardcoding a path because Flask handles the URL generation for you, even if the app is deployed under a different root URL later.
- Save index.html.
- Refresh your browser at http://127.0.0.1:5000.
You should see your URL shortener styled as a white card centered on a light gray background. The form input and Shorten button sit side by side.
- Submit a test URL through the form to confirm the result box also appears styled with a light blue background.
Page still looks unstyled?
- Check that your static/ folder is at the same level as app.py, not inside the templates/ folder.
- Make sure the filename in your <link> tag says style.css and matches the actual file name exactly.
- Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). Browsers sometimes cache old versions of CSS files.
Help me figure out why my CSS styles aren't showing up.
✔️ Awesome, I've got everything!
Make sure you've saved index.html before moving on.
ⓧ I'd like to double check the full code
Here's the complete templates/index.html file. Compare it against yours and make sure everything matches.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Shortener</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<h1>URL Shortener</h1>
<p>Paste a long URL below to get a short link.</p>
<form method="POST">
<input type="text" name="original_url" placeholder="https://example.com/very/long/url" required>
<button type="submit">Shorten</button>
</form>
{% if short_url %}
<div class="result">
<p>Your shortened URL:</p>
<a href="{{ short_url }}" target="_blank">{{ short_url }}</a>
</div>
{% endif %}
</div>
</body>
</html>
Secret mission
Add Click Tracking and a Stats Page
Your URL shortener works, but you have no idea which links are getting clicked. Time to add analytics. Track every click on your short URLs and build a stats dashboard to see which links get the most traffic.
Before you go
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The url-shortener project directory on your Desktop.
- A Python virtual environment (.venv) inside the project directory.
- Flask 3.1.3 installed inside the virtual environment.
- A urls.db SQLite database file storing your shortened URLs.
- Application files: app.py, templates/index.html, and static/style.css.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep experimenting with your URL shortener.
- Your project directory, virtual environment, and database all stay in place.
- To start the server again later, open your terminal and run:
cd ~/Desktop/url-shortener
source .venv/bin/activate
flask run --debug
- Visit http://127.0.0.1:5000 in your browser. All your previously shortened URLs are still saved in the database.
Pause - I'll come back to this later
Shut down the Flask server and deactivate the virtual environment. Your project files and database stay on disk so you can pick up where you left off.
- If the Flask server is still running in your terminal, press Ctrl+C to stop it.
- Deactivate the virtual environment by running:
deactivate
- Your url-shortener folder, virtual environment, and urls.db database remain on your Desktop. Nothing is lost.
- To resume later, reactivate the environment and start the server:
cd ~/Desktop/url-shortener
source .venv/bin/activate
flask run --debug
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes everything including the virtual environment, database, and all your code files.
- If the Flask server is still running, press Ctrl+C to stop it.
- Deactivate the virtual environment by running:
deactivate
- Delete the entire project directory by running:
rm -rf ~/Desktop/url-shortener
- This removes the url-shortener folder, the .venv virtual environment, the urls.db database, and all application files.
Mission accomplished
That's a Wrap!
Nice work! You just built a fully functional URL shortener from scratch using Python and Flask. You can paste in any long URL, get back a short link, and click it to redirect to the original destination.
You've learned how to:
- Built a Flask web server with multiple routes that handle GET and POST requests, short code generation with collision avoidance, and HTTP redirects.
- Created a SQLite database with a schema for storing URL mappings, short codes, and timestamps that persist between server restarts.
- Built a styled HTML frontend with Jinja templating and CSS for submitting URLs and displaying shortened links.
- Secret Mission: Added click tracking and a stats dashboard to monitor how many times each short link has been visited.
Ready to quiz yourself?