Build a Local URL Shortener with Flask
Build a Flask URL shortener with SQLite persistence and click tracking.
Introduction
30 Second Summary
Long web addresses are awkward to copy. One missing character can send someone nowhere.
In this project, you will build PocketLink, a local URL shortener with Flask. It creates compact links that still work after the app restarts.
What You'll Build
PocketLink turns a long web address into a compact local link that opens the original page in your browser.
By the end of this project, you'll have:
- A complete shortening flow where a full web address becomes a compact local link that opens the original page.
- Short links that survive restarts because SQLite stores every mapping on your computer.
- Clear guardrails that reject malformed web addresses and explain when a short code does not exist.
- Secret Mission: Record every redirect and show each short link's click count on the home page.
Are there any prerequisites?
You'll need a Windows computer with Python 3.9 or newer, Visual Studio Code, and Microsoft Edge already installed. Everything runs locally, so you do not need a cloud account or paid service.
Before We Start
Before the hands-on work begins, this is your moment to commit to building PocketLink. Giving the project a clear purpose makes every new Python concept easier to connect to a visible result.
Set Up PocketLink
PocketLink needs a predictable place to run before it handles browser requests. A virtual environment keeps Flask 3.1.3 separate from packages used by your other Python projects.
This step creates that workspace in Visual Studio Code. You will finish by serving a test response through Command Prompt in Microsoft Edge.
In this step, get ready to:
- Create the pocketlink workspace in Visual Studio Code.
- Isolate Flask 3.1.3 inside .venv.
- Serve a minimal PocketLink page in Microsoft Edge.
Create the PocketLink workspace
A workspace keeps the project files within one named folder. Visual Studio Code also starts its integrated terminal inside that folder.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Select File from the menu bar.
- Select Open Folder....
- Select New Folder in the folder dialog.
- Enter pocketlink as the folder name.
- Select Select Folder.
Good. Your empty pocketlink workspace now gives every project file a clear home.
- Confirm that you trust the folder if Visual Studio Code shows a workspace trust dialog.
- Select Terminal from the menu bar.
- Select New Terminal.
- Open the profile dropdown in the terminal panel.
- Select Command Prompt as the terminal profile.
- Check the available Python version by running this command:
py -V
What Does This Command Check?
The py launcher selects Python on Windows. The -V option prints the selected version.
✔️ I see version 3.9 or newer
You are set. This Python version meets Flask's minimum requirement.
ⓧ I see an older version
Your installed version is below 3.9. Install a current Python release before creating PocketLink's environment.
- Open the official Python Install Manager page.
- Choose the Microsoft Store installation option.
- Complete the installation prompts.
- Return to Visual Studio Code.
- Create a new Command Prompt terminal.
- Check the version again by running this command:
py -V
What Should I See?
The command should report Python 3.9 or newer before you continue.
ⓧ Command not found
Command Prompt cannot find the Python launcher. Install Python through its official Windows install manager.
- Open the official Python Install Manager page.
- Choose the Microsoft Store installation option.
- Complete the installation prompts.
- Return to Visual Studio Code.
- Create a new Command Prompt terminal.
- Check the installation by running this command:
py -V
What Should I See?
The command should print a Python version of 3.9 or newer.
Isolate Flask in a virtual environment
A virtual environment gives PocketLink its own package area. Activating .venv makes later Python commands use that isolated environment.
- Create .venv inside the pocketlink workspace by running this command:
py -3 -m venv .venv
What Does This Command Do?
The command creates a Python virtual environment in the .venv folder. Packages installed there stay scoped to PocketLink.
- Confirm that .venv appears in the Explorer sidebar.
Does .venv Fail to Appear?
Confirm that the terminal path ends with pocketlink. A different path creates the environment outside your project folder.
Check that the version command succeeded before retrying the environment command.
Help me create the PocketLink virtual environment.
- Activate the new environment by running this command:
.venv\Scripts\activate
What Does Activation Change?
Activation updates the current terminal session so Python package commands target .venv.
The environment name now appears at the beginning of your Command Prompt line. That visible prefix confirms PocketLink's isolated Python environment is active.
Does Activation Fail?
Confirm that the terminal uses Command Prompt. The activation command in this project is written for that shell.
Confirm that the .venv folder appears directly inside pocketlink.
Help me activate .venv in Command Prompt.
The active environment is ready for PocketLink's dependency list. Pinning the Flask version makes the same package release install each time.
- Select the New File... button in the Explorer sidebar.
- Enter requirements.txt as the file name.
- Pin PocketLink's Flask dependency by adding this line:
Flask==3.1.3
Why Pin Flask?
The double equals sign requests the exact Flask release recorded in requirements.txt. This keeps PocketLink's setup repeatable.
- Save requirements.txt.
✔️ Awesome, I've got everything!
Great. Your saved dependency file now pins the Flask version used by PocketLink.
ⓧ I'd like to double check the full code
Compare your saved requirements.txt with this complete file.
Flask==3.1.3
The file contains one exact package pin followed by a final line break.
- Install the dependency from requirements.txt by running this command:
python -m pip install -r requirements.txt
What Does This Command Do?
The command asks pip to read requirements.txt. It installs Flask into the active virtual environment.
The terminal returns to the prompt after listing Flask 3.1.3 among the installed packages. That is your isolated dependency setup complete.
Does the Flask Install Fail?
Confirm that the environment name appears at the beginning of the terminal prompt. Reactivate .venv if it is missing.
Confirm that requirements.txt is saved directly inside pocketlink.
Help me diagnose the Flask installation.
Serve the first PocketLink page
A Flask route connects a browser path to a Python function. PocketLink's first route returns one plain-text response so you can prove the server works.
- Select the New File... button in the Explorer sidebar.
- Enter app.py as the file name.
- Build the home route by adding this code:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "PocketLink is running."
What Does This Code Do?
- The Flask import gives the file access to the application class.
- The app object represents the PocketLink web application.
- The @app.route("/") decorator connects the home path to index().
- The index() function sends PocketLink's test message back to the browser.
- Save app.py.
- Confirm that app.py appears beside requirements.txt in the Explorer sidebar.
✔️ Awesome, I've got everything!
Your saved application file now contains PocketLink's first home route.
ⓧ I'd like to double check the full code
Compare your saved app.py with this complete file.
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "PocketLink is running."
The complete file creates one Flask application with one home route.
The development server keeps this terminal occupied while PocketLink runs. You can stop it later with Ctrl+C.
Before you start the server, what do you expect the browser to show at PocketLink's home address?
- Start the PocketLink development server by running this command:
flask run
What Does This Command Start?
The command finds the Flask application in app.py. It starts a local development server for browser testing.
This server is designed for development. PocketLink remains local to your computer in this project.
The terminal prints PocketLink's local development address. It stays busy while the server listens for browser requests.
- Press the Windows key to open Windows search.
- Type Microsoft Edge into the search field.
- Press Enter to open Microsoft Edge.
- Enter http://127.0.0.1:5000/ in the address bar.
- Press Enter to load PocketLink.
You should see PocketLink is running. on the page. You have served your first visible response from PocketLink.
Does the Page Fail to Load?
Confirm that the terminal running flask run remains open. Stopping that process also stops the local page.
Confirm that app.py is saved inside the open pocketlink workspace.
Help me diagnose the PocketLink page.
Your isolated Flask environment now serves PocketLink in the browser. Next, you will turn this one-line response into a form that creates temporary short links.
Shorten and Redirect In Memory
Your Flask development server already proves that PocketLink can answer a browser request. Now you can turn that single response into a complete shortening journey.
A temporary Python dictionary will connect each short code to its destination. A restart test will reveal whether that state lasts.
In this step, get ready to:
- Accept long URLs through a browser form.
- Generate short codes that redirect to their saved destinations.
- Test what happens to an in-memory mapping after a server restart.
Build the in-memory shortening route
The home route handles both page loads and form submissions. Each submitted URL receives a six-character code from secrets.token_hex(3).
- Return to the integrated Command Prompt terminal from earlier.
- Stop the running development server by pressing Ctrl+C.
- Select app.py in the Explorer sidebar.
- Replace the current contents of app.py with this code:
import secrets
from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
links = {}
def create_code():
return secrets.token_hex(3)
@app.route("/", methods=["GET", "POST"])
def index():
short_url = None
original_url = ""
if request.method == "POST":
original_url = request.form["url"].strip()
code = create_code()
links[code] = original_url
short_url = url_for("follow_link", code=code, _external=True)
return render_template(
"index.html",
short_url=short_url,
original_url=original_url,
)
What Does This Code Do?
- The links dictionary stores each generated code as a key. Its destination URL becomes the matching value.
- The create_code() function generates six hexadecimal characters from three random bytes.
- The index() route reads the submitted url field during a POST request.
- The url_for() call builds a complete local link for the generated code.
- Save app.py.
- Confirm that app.py begins with import secrets.
- Confirm that the file ends with the render_template() call.
Seeing a Problem in app.py?
Check that links = {} sits below the Flask application. Confirm that every line inside index() uses four spaces of indentation.
Make sure the old PocketLink is running. response has been removed.
Help me check my in-memory PocketLink route.
Create and style the browser form
A template turns the route data into a browser page. The form sends a long URL back to the home route.
- Select the pocketlink folder in the Explorer sidebar.
- Use the new-folder control above the file tree.
- Name the folder templates.
- Select the new templates folder.
- Use the new-file control above the file tree.
- Name the file index.html.
- Add the browser form by pasting this code into templates/index.html.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PocketLink</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="card">
<p class="eyebrow">Local Flask project</p>
<h1>PocketLink</h1>
<p class="intro">Paste a long URL and get a compact local link that redirects to it.</p>
<form method="post">
<label for="url">Long URL</label>
<input id="url" name="url" type="text" value="{{ original_url }}" placeholder="https://example.com/a/very/long/path" required>
<button type="submit">Shorten URL</button>
</form>
{% if short_url %}
<section class="result">
<h2>Your short link</h2>
<a href="{{ short_url }}">{{ short_url }}</a>
<p>Destination: {{ original_url }}</p>
</section>
{% endif %}
</main>
</body>
</html>
How Does the Template Work?
- The form sends its url field to the current route with a POST request.
- The original_url value keeps the submitted destination visible in the input.
- The short_url condition displays the result section after the route generates a link.
- The stylesheet link connects this template to static/style.css.
- Save templates/index.html.
- Confirm that index.html appears inside the templates folder.
Template File in the Wrong Place?
Confirm that the path shown in Explorer is templates/index.html. A file beside the templates folder cannot be found by the route.
Help me check my PocketLink template location.
✔️ Awesome, I've got everything!
Great. Your saved template now contains the URL form plus the conditional result section.
ⓧ I'd like to double check the full code
Compare templates/index.html with this complete file.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PocketLink</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="card">
<p class="eyebrow">Local Flask project</p>
<h1>PocketLink</h1>
<p class="intro">Paste a long URL and get a compact local link that redirects to it.</p>
<form method="post">
<label for="url">Long URL</label>
<input id="url" name="url" type="text" value="{{ original_url }}" placeholder="https://example.com/a/very/long/path" required>
<button type="submit">Shorten URL</button>
</form>
{% if short_url %}
<section class="result">
<h2>Your short link</h2>
<a href="{{ short_url }}">{{ short_url }}</a>
<p>Destination: {{ original_url }}</p>
</section>
{% endif %}
</main>
</body>
</html>
How to Compare the File
Check the document structure first. Confirm that the form appears before the conditional result section.
Before you start the server, what should replace the one-line setup page in the browser?
- Return to the integrated Command Prompt terminal.
- Start the updated Flask application by running this command:
flask run
What Does This Command Do?
The command starts the local development server with the updated route. The terminal remains busy while PocketLink handles browser requests.
- Switch back to Microsoft Edge.
- Refresh http://127.0.0.1:5000/.
You will see the PocketLink heading. You will also see the Long URL field plus the Shorten URL button.
Still Seeing the Setup Message?
Confirm that you stopped the earlier server before running the command again. Check that the terminal is working inside the pocketlink folder.
Help me load my updated PocketLink form.
The form works as raw browser content. A CSS file will turn it into a focused PocketLink card.
- Select the pocketlink folder in the Explorer sidebar.
- Use the new-folder control above the file tree.
- Name the folder static.
- Select the new static folder.
- Use the new-file control above the file tree.
- Name the file style.css.
- Add the page-wide colors plus sizing rules by pasting this code into static/style.css.
:root {
font-family: "Segoe UI", sans-serif;
color: #172033;
background: #edf2f7;
}
* {
box-sizing: border-box;
}
What Do These Styles Control?
- The :root selector sets the page font plus the default foreground and background colors.
- The universal selector includes borders plus padding inside each element's declared size.
- Save static/style.css.
- Refresh Microsoft Edge.
You will see a pale background. The page text now uses the Segoe UI font.
Colors Not Changing?
Confirm that the stylesheet path is static/style.css. Check that the stylesheet link remains inside the template's head element.
Help me connect my PocketLink stylesheet.
- Add the page layout below the existing styles in static/style.css.
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
}
What Does the Body Rule Do?
The grid layout centers PocketLink within the browser window. The padding keeps the content away from the window edges.
- Save static/style.css.
- Refresh Microsoft Edge.
You will see the form move to the center of the browser window.
Form Still at the Top?
Check that display: grid; sits inside the body rule. Confirm that the rule includes min-height: 100vh;.
Help me center the PocketLink page.
- Add the card styles below the body rule.
.card {
width: min(680px, 100%);
background: #ffffff;
border-radius: 18px;
padding: 32px;
box-shadow: 0 18px 45px rgba(23, 32, 51, 0.12);
}
What Does the Card Rule Do?
The .card rule limits the content width. Its white background plus shadow separates PocketLink from the page.
- Save static/style.css.
- Refresh Microsoft Edge.
You will see PocketLink inside a white card with rounded corners plus a soft shadow.
Card Still Looks Like Plain Text?
Confirm that the template uses class="card" on the main element. Check that the CSS selector begins with a full stop.
Help me style the PocketLink card.
- Add the form layout below the card styles.
form {
display: grid;
gap: 12px;
margin-top: 28px;
}
What Does the Form Rule Do?
The form uses a grid to stack its label plus controls. The gap creates consistent space between those elements.
- Save static/style.css.
- Refresh Microsoft Edge.
You will see clear spacing between the label, input, and button.
Form Controls Still Crowded?
Confirm that the form selector has no leading full stop. Check that gap: 12px; sits inside its braces.
Help me fix my PocketLink form spacing.
- Add the shared input plus button sizing below the form rule.
input,
button {
width: 100%;
border-radius: 10px;
padding: 13px 14px;
font: inherit;
}
Why Share These Styles?
The grouped selectors give both controls the same width plus corner shape. Inherited typography keeps their text consistent with the card.
- Save static/style.css.
- Refresh Microsoft Edge.
You will see a full-width input plus a full-width button with matching rounded corners.
Controls Have Different Widths?
Check that a comma follows the input selector. Confirm that both selectors share one pair of braces.
Help me align my PocketLink form controls.
- Add the button colors below the shared control styles.
button {
border: 0;
color: #ffffff;
background: #6d28d9;
cursor: pointer;
}
What Does the Button Rule Do?
The button receives a purple background plus white text. The pointer cursor signals that the control is clickable.
- Save static/style.css.
- Refresh Microsoft Edge.
You will see a purple Shorten URL button with white text.
Button Still Uses Browser Colors?
Confirm that the selector is button without a full stop. Check that each color begins with a hash symbol.
Help me fix the PocketLink button styles.
- Add the generated-result styles at the end of static/style.css.
.result {
margin-top: 22px;
padding: 16px;
border-radius: 10px;
background: #ede9fe;
overflow-wrap: anywhere;
}
What Does the Result Rule Do?
The .result rule gives generated links their own panel. Long local links can wrap inside the card.
- Save static/style.css.
- Confirm that the file ends with the .result rule.
Result Rule Missing From the File?
Check that the .result rule appears after the button rule. Confirm that every declaration remains inside its braces.
Help me check my PocketLink result styles.
✔️ Awesome, I've got everything!
Your saved stylesheet now covers the page, card, form, controls, and generated result.
ⓧ I'd like to double check the full code
Compare static/style.css with this complete file.
:root {
font-family: "Segoe UI", sans-serif;
color: #172033;
background: #edf2f7;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
}
.card {
width: min(680px, 100%);
background: #ffffff;
border-radius: 18px;
padding: 32px;
box-shadow: 0 18px 45px rgba(23, 32, 51, 0.12);
}
form {
display: grid;
gap: 12px;
margin-top: 28px;
}
input,
button {
width: 100%;
border-radius: 10px;
padding: 13px 14px;
font: inherit;
}
button {
border: 0;
color: #ffffff;
background: #6d28d9;
cursor: pointer;
}
.result {
margin-top: 22px;
padding: 16px;
border-radius: 10px;
background: #ede9fe;
overflow-wrap: anywhere;
}
How to Compare the Stylesheet
Check each selector in order. Confirm that every opening brace has a matching closing brace.
Redirect through a short code
A dynamic route treats part of the browser path as a value. PocketLink will use that value to retrieve a destination from links.
- Switch back to app.py.
- Add this route below the index() function:
@app.route("/<code>")
def follow_link(code):
original_url = links.get(code)
if original_url is None:
return "Short link not found.", 404
return redirect(original_url)
How Does the Redirect Route Work?
- The <code> path segment becomes the code argument passed to follow_link().
- The links.get(code) lookup retrieves the saved destination for that code.
- A missing mapping returns Short link not found. with status 404.
- A matching mapping passes its destination to redirect().
- Save app.py.
- Confirm that follow_link() appears below index().
Route Showing an Editor Error?
Confirm that the route begins at the left edge of the file. Check that each line inside follow_link() uses four spaces of indentation.
Help me check my dynamic redirect route.
✔️ Awesome, I've got everything!
Your completed app.py now creates mappings plus follows generated codes.
ⓧ I'd like to double check the full code
Compare app.py with this complete file.
import secrets
from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
links = {}
def create_code():
return secrets.token_hex(3)
@app.route("/", methods=["GET", "POST"])
def index():
short_url = None
original_url = ""
if request.method == "POST":
original_url = request.form["url"].strip()
code = create_code()
links[code] = original_url
short_url = url_for("follow_link", code=code, _external=True)
return render_template(
"index.html",
short_url=short_url,
original_url=original_url,
)
@app.route("/<code>")
def follow_link(code):
original_url = links.get(code)
if original_url is None:
return "Short link not found.", 404
return redirect(original_url)
How to Compare the Application
Check the imports plus application setup first. Confirm that create_code() appears before the two routes.
Confirm that index() stores each mapping. Confirm that follow_link() retrieves the same mapping.
- Return to the integrated Command Prompt terminal.
- Stop the running server by pressing Ctrl+C.
- Restart PocketLink with the completed redirect route by running this command:
flask run
Why Restart the Server Now?
The restarted process loads the completed app.py file. PocketLink can now match generated paths with follow_link().
Server Does Not Start?
Check the line reported in the terminal. A missing parenthesis or incorrect indentation in app.py prevents Flask from loading the file.
Help me diagnose my PocketLink startup problem.
Before you submit a URL, where do you expect the generated short link to take you?
- Switch back to Microsoft Edge.
- Enter http://127.0.0.1:5000/ in the address bar.
- Load the PocketLink home page.
- Enter https://example.com in the Long URL field.
- Select Shorten URL.
You will see a purple result panel containing a local short link. The panel also shows https://example.com as its destination.
- Copy the displayed link into this project note: your generated short link.
- Select the generated short link.
You will arrive at the original destination. That is the full shortening loop working from form submission to redirect.
Short Link Does Not Redirect?
Confirm that you selected the generated local link instead of the destination text. Check that the terminal is still running PocketLink.
Help me diagnose my PocketLink redirect.
Before restarting PocketLink, do you think the same short link will still know its destination?
- Return to the integrated Command Prompt terminal.
- Stop the server by pressing Ctrl+C.
- Start a fresh PocketLink process by running this command:
flask run
What Changed During the Restart?
The command starts a new Python process. The new process creates a fresh links dictionary when it loads app.py.
- Switch back to Microsoft Edge.
- Enter your generated short link in the address bar.
- Load the old short link.
You will see Short link not found. The planned restart has erased the temporary mapping.
Why Did the Mapping Disappear?
The links dictionary lives inside one running Python process. Stopping that process removes every code-to-URL pair it held.
The files remain on disk. The generated mapping does not.
Project Checkpoint
- The unchanged requirements.txt file still contains Flask==3.1.3.
- The completed app.py file contains the in-memory mapping plus both routes.
- The templates/index.html file contains the form plus generated result.
- The static/style.css file styles the PocketLink page.
You have proved that PocketLink can shorten and redirect a URL. Next, you will replace its temporary memory with SQLite so the same links survive a restart.
Make Links Survive a Restart
Your shortener completed a full redirect in the last step. The restart test exposed that its dictionary forgets every mapping.
Now you will move each mapping into a SQLite database on disk. Flask will manage each database connection through the current application context.
In this step, get ready to:
- Create a request-scoped SQLite connection plus a persistent links table.
- Replace dictionary access with parameterized SQL.
- Restart Flask to prove a saved mapping remains available.
Build the database connection and schema
A file-backed database stores mappings outside Python's process memory. The get_db() helper keeps one connection in the request context.
The teardown helper closes that connection after Flask finishes the request. This keeps connection management in one predictable place.
- Select app.py in the Visual Studio Code Explorer sidebar.
- Select the code from import secrets through links = {}.
- Replace the selected code with this database connection setup:
import secrets
import sqlite3
from flask import Flask
from flask import current_app, g
from flask import redirect, url_for
from flask import render_template
from flask import request
app = Flask(__name__)
app.config["DATABASE"] = "pocketlink.sqlite"
def get_db():
if "db" not in g:
g.db = sqlite3.connect(current_app.config["DATABASE"])
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception=None):
db = g.pop("db", None)
if db is not None:
db.close()
What Does This Code Do?
- The DATABASE setting stores the name of the local database file.
- The get_db() helper opens pocketlink.sqlite when the current request first needs it.
- The sqlite3.Row row factory lets later code read a column by its name.
- The close_db() helper closes the stored connection when Flask clears the application context.
- Save app.py.
- Return to the integrated Command Prompt terminal.
- Press Ctrl+C to stop the running server.
- Restart the server with the new connection code by running:
flask run
What Does This Command Check?
Flask imports the edited app.py before starting the local server. A successful start confirms that the new imports plus connection helpers have valid Python syntax.
- Switch back to Microsoft Edge.
- Refresh the PocketLink page.
You should see the URL form again. The page confirms that Flask can load the database connection code.
Does the Home Page Fail to Load?
Check the indentation inside get_db() plus close_db(). Confirm that app.config["DATABASE"] appears directly below the Flask app creation.
Compare every imported name with the code block above. A missing import prevents Flask from loading app.py.
Help me diagnose the database connection setup.
The connection can now reach a database file. The next helper gives that database a table for short-code mappings.
- Switch back to app.py in Visual Studio Code.
- Place your cursor below the db.close() line.
- Add the table initialization code below:
def init_db():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS links (
code TEXT PRIMARY KEY,
original_url TEXT NOT NULL
)
"""
)
db.commit()
with app.app_context():
init_db()
How Is the Table Created?
- The links table stores one short code plus one original URL in each row.
- The PRIMARY KEY constraint keeps every short code unique.
- The NOT NULL constraint requires every row to have an original destination.
- The app.app_context() block makes the database configuration available while Flask initializes the table.
- The commit() call saves the table creation to disk.
- Save app.py.
- Return to the integrated terminal.
- Press Ctrl+C to stop Flask.
- Create the database during the next server start by running:
flask run
What Happens During Startup?
Flask enters the application context before calling init_db(). SQLite creates pocketlink.sqlite plus the links table if they do not already exist.
- Return to the Visual Studio Code Explorer sidebar.
- Confirm that pocketlink.sqlite appears inside the pocketlink folder.
The new file is your first visible proof that mappings can live beyond one Python process.
Is the Database File Missing?
Confirm that the with app.app_context(): block appears directly below init_db(). Save app.py before restarting Flask.
Check the integrated terminal for a Python error mentioning the edited database setup. Fix the reported indentation or spelling before running the server again.
Help me create the SQLite database file.
Store and retrieve links with SQL
The table exists on disk. PocketLink now needs queries that write each mapping into that table plus retrieve it during a redirect.
Parameterized queries keep SQL instructions separate from their values. An explicit commit makes every inserted mapping persistent.
- Switch back to app.py.
- Find the def create_code(): function.
- Select the complete function through return secrets.token_hex(3).
- Replace it with this database-aware version:
def create_code():
db = get_db()
while True:
code = secrets.token_hex(3)
existing = db.execute(
"SELECT 1 FROM links WHERE code = ?", (code,)
).fetchone()
if existing is None:
return code
How Does Code Generation Change?
- The loop generates a six-character code with secrets.token_hex(3).
- The SELECT query checks whether the candidate already exists in the table.
- The question mark receives the code through a separate parameter tuple.
- The function returns the candidate only when no matching row exists.
- Save app.py.
- Return to the integrated terminal.
- Press Ctrl+C to stop Flask.
- Load the database-aware code generator by running:
flask run
What Does This Restart Confirm?
The server imports the updated create_code() function. A successful start confirms that its database query has valid Python syntax.
The terminal should start the local server without a traceback. Your code generator can now check the persistent table.
Does the Server Stop During Startup?
Check that the parameter after the SQL string is written as (code,). The comma makes it a one-item tuple.
Confirm that fetchone() remains attached to the database query. Check the parentheses around the complete expression.
Help me fix the database-aware code generator.
- Return to app.py.
- Find the def index(): function.
- Select the complete function through its closing render_template() call.
- Replace it with this database insert flow:
@app.route("/", methods=["GET", "POST"])
def index():
short_url = None
original_url = ""
if request.method == "POST":
original_url = request.form["url"].strip()
code = create_code()
db = get_db()
db.execute(
"INSERT INTO links (code, original_url) VALUES (?, ?)",
(code, original_url),
)
db.commit()
short_url = url_for("follow_link", code=code, _external=True)
return render_template(
"index.html",
short_url=short_url,
original_url=original_url,
)
How Is a Mapping Saved?
- The route gets a database connection after generating an unused code.
- The INSERT query stores the code plus its original URL as one row.
- The values travel separately through the parameter tuple.
- The commit() call writes the new row to pocketlink.sqlite.
- Save app.py.
- Return to the integrated terminal.
- Press Ctrl+C to stop Flask.
- Load the updated home route by running:
flask run
What Does This Restart Load?
The new server process loads the database insert flow. Each form submission can now create a durable row before Flask builds the short URL.
- Switch back to Microsoft Edge.
- Refresh the PocketLink page.
- Enter https://example.com/a/very/long/path in the Long URL field.
- Select Shorten URL.
You should see a generated local link plus the original destination in the result panel. That mapping now exists inside pocketlink.sqlite.
- Record the generated link here: your generated PocketLink URL.
Does the Form Submission Fail?
Check that the INSERT statement has two question marks. Confirm that the following tuple contains code plus original_url in the same order.
Make sure db.commit() appears after the insert. Check the terminal for the line number of any indentation error.
Help me fix the SQLite insert.
- Return to app.py.
- Find the def follow_link(code): function.
- Select the complete function through return redirect(original_url).
- Replace it with this database lookup flow:
@app.route("/<code>")
def follow_link(code):
link = get_db().execute(
"SELECT original_url FROM links WHERE code = ?", (code,)
).fetchone()
if link is None:
return "Short link not found.", 404
return redirect(link["original_url"])
How Does the Redirect Find Its Destination?
- The SELECT query retrieves the destination stored for the route's code.
- The fetchone() call returns one matching row or None.
- The missing-row branch keeps the existing not-found response.
- The redirect reads the saved destination from link["original_url"].
- Remove the trailing space after the final closing parenthesis if your editor inserted one.
- Save app.py.
- Return to the integrated terminal.
- Press Ctrl+C to stop Flask.
- Load the completed database lookup by running:
flask run
What Does This Restart Load?
The running app now uses SQLite for code generation plus inserts plus redirect lookups. The in-memory dictionary is no longer part of the request flow.
- Switch back to Microsoft Edge.
- Select the generated short link in the result panel.
Microsoft Edge should leave the local PocketLink address. You should see the browser load the recorded destination.
Does the Short Link Show a Server Error?
Confirm that the query selects original_url from links. Check that the redirect reads link["original_url"].
Make sure the lookup parameter is written as (code,). A one-value query still needs a tuple.
Help me fix the database redirect lookup.
✔️ Awesome, I've got everything!
Great. Save app.py before the persistence test.
ⓧ I'd like to double check the full code
import secrets
import sqlite3
from flask import Flask
from flask import current_app, g
from flask import redirect, url_for
from flask import render_template
from flask import request
app = Flask(__name__)
app.config["DATABASE"] = "pocketlink.sqlite"
def get_db():
if "db" not in g:
g.db = sqlite3.connect(current_app.config["DATABASE"])
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception=None):
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS links (
code TEXT PRIMARY KEY,
original_url TEXT NOT NULL
)
"""
)
db.commit()
with app.app_context():
init_db()
def create_code():
db = get_db()
while True:
code = secrets.token_hex(3)
existing = db.execute(
"SELECT 1 FROM links WHERE code = ?", (code,)
).fetchone()
if existing is None:
return code
@app.route("/", methods=["GET", "POST"])
def index():
short_url = None
original_url = ""
if request.method == "POST":
original_url = request.form["url"].strip()
code = create_code()
db = get_db()
db.execute(
"INSERT INTO links (code, original_url) VALUES (?, ?)",
(code, original_url),
)
db.commit()
short_url = url_for("follow_link", code=code, _external=True)
return render_template(
"index.html",
short_url=short_url,
original_url=original_url,
)
@app.route("/<code>")
def follow_link(code):
link = get_db().execute(
"SELECT original_url FROM links WHERE code = ?", (code,)
).fetchone()
if link is None:
return "Short link not found.", 404
return redirect(link["original_url"])
What Should Match?
Your complete file should define the database lifecycle before the routes. The three request functions should use parameterized queries instead of the old links dictionary.
Prove the link survives a restart
A fresh redirect proves that the database flow works during one server session. The decisive test ends that process before reopening the same saved link.
- Return to the integrated Command Prompt terminal.
- Press Ctrl+C to stop Flask.
Before you restart Flask, do you think the recorded short link still has a destination after its original server process has ended?
- Start a new Flask process by running:
flask run
What Does This Restart Test?
This command creates a new Python process. The new process reopens pocketlink.sqlite from disk instead of recovering the previous process's memory.
- Switch back to Microsoft Edge.
- Select the browser address bar.
- Enter your generated PocketLink URL.
- Press Enter.
You should see Microsoft Edge load the same original destination. The old short link works because its mapping came from the database file.
- Switch back to the Visual Studio Code Explorer sidebar.
- Confirm that pocketlink.sqlite remains inside the pocketlink folder.
You have crossed the persistence gap. Your short links now survive the process that created them.
Does the Old Link Show as Missing?
Confirm that you reopened the exact short URL recorded before the restart. A newly typed code has no matching database row.
Check that db.commit() appears after the INSERT query. Without that commit, the mapping does not reach the database file.
Help me diagnose the persistence test.
Your short links now survive a Flask restart. Next, you will add clear feedback for malformed destinations plus unknown codes.
Reject Bad URLs and Explain Missing Links
Your SQLite backed short links now survive a server restart. Known codes keep redirecting to their saved destinations.
A Flask redirector still needs guardrails for malformed destinations. It also needs a useful response when a visitor opens an unknown code.
In this step, get ready to:
- Accept only full HTTP or HTTPS destinations.
- Show a clear message when a submitted URL is malformed.
- Render a dedicated page for unknown short codes.
Validate submitted URLs
The urlsplit() parser separates a URL into components. PocketLink can check the scheme plus the network location before storing a destination.
- In the app.py tab from earlier, find the existing import sqlite3 line.
- Add the parser import directly below it by copying this line:
from urllib.parse import urlsplit
What Does This Import Provide?
The import makes urlsplit() available in app.py. The function separates submitted text into URL components that your validator can inspect.
- Find the existing create_code() function.
- Add the validation function directly above it by copying this code:
def is_valid_url(value):
try:
parsed = urlsplit(value)
except ValueError:
return False
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
What Does This Validator Check?
- The try block parses the submitted value while allowing malformed parser input to return False.
- The scheme check accepts the literal http or https values.
- The netloc check requires a network location such as a host name.
- Save app.py.
- Switch back to the integrated terminal from earlier.
- Confirm the development server reloads after the saved change.
- Refresh PocketLink in Microsoft Edge.
You should still see the PocketLink form. This confirms the parser import plus the new function load without a syntax error.
Does the Server Stop Reloading?
Check that from urllib.parse import urlsplit appears with the other imports. Confirm that the new function sits above create_code().
Compare the indentation inside try plus except. Python requires both blocks to align correctly.
Help me fix my URL validation function.
- In app.py, select the existing index() route from its decorator through its closing parenthesis.
- Replace the selected route by copying this version:
@app.route("/", methods=["GET", "POST"])
def index():
error = None
short_url = None
original_url = ""
if request.method == "POST":
original_url = request.form["url"].strip()
if not is_valid_url(original_url):
error = "Enter a full URL starting with http:// or https://."
else:
code = create_code()
db = get_db()
db.execute(
"INSERT INTO links (code, original_url) VALUES (?, ?)",
(code, original_url),
)
db.commit()
short_url = url_for("follow_link", code=code, _external=True)
return render_template(
"index.html",
error=error,
short_url=short_url,
original_url=original_url,
)
How Does the Route Protect the Database?
- The error variable starts empty for every request.
- Invalid input receives the full-URL message before PocketLink creates a code.
- The else block keeps the parameterized insert plus its commit on the valid path.
- The template receives error so the page can explain the rejection.
- Save app.py.
- Switch back to PocketLink in Microsoft Edge.
- Enter example.com in the Long URL field.
- Select Shorten URL.
You should see the submitted value remain in the field. You should not see a generated short link.
Does Invalid Input Still Create a Link?
Confirm that code generation plus the database insert sit inside the else block. Check that is_valid_url(original_url) includes the leading not in the condition.
Help me stop invalid URLs from being stored.
Show clear feedback in the form
The route now rejects malformed input before it reaches the database. The page needs to display the reason beside the form.
- Switch back to templates/index.html in the Visual Studio Code editor.
- Select the existing form plus result sections.
- Replace the selection by copying this markup:
<form method="post">
<label for="url">Long URL</label>
<input
id="url"
name="url"
type="text"
value="{{ original_url }}"
placeholder="https://example.com/a/very/long/path"
required
>
<button type="submit">Shorten URL</button>
</form>
{% if error %}
<p class="message error" role="alert">{{ error }}</p>
{% endif %}
{% if short_url %}
<section class="result" aria-live="polite">
<h2>Your short link</h2>
<a href="{{ short_url }}">{{ short_url }}</a>
<p>Destination: {{ original_url }}</p>
</section>
{% endif %}
How Does the Template Surface Feedback?
- The {% if error %} block adds the message only when the route supplies one.
- The {{ error }} expression renders the route's full-URL message.
- The result section keeps the generated link separate from validation feedback.
- Save templates/index.html.
- Refresh PocketLink in Microsoft Edge.
- Enter example.com in the Long URL field.
- Select Shorten URL.
You should see Enter a full URL starting with http:// or https://. below the form. The page now explains why PocketLink rejected the input.
Is the Message Missing?
Confirm that the template condition uses error. Check that the route passes error=error into render_template().
Help me display the validation message.
- Switch back to static/style.css.
- Find the closing brace of the existing .card block.
- Add these typography styles directly below it by copying this code:
.eyebrow {
margin: 0;
color: #5b21b6;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
h1 {
margin: 8px 0;
font-size: clamp(2rem, 8vw, 3.5rem);
}
.intro {
color: #526077;
}
What Do These Styles Change?
The eyebrow becomes a compact purple label above the title. The heading plus introduction gain a clearer visual hierarchy inside the card.
- Select the CSS section that begins with input, button.
- Extend the selection through the existing button block.
- Replace the selection by copying these form styles:
label {
font-weight: 700;
}
input,
button {
width: 100%;
border-radius: 10px;
padding: 13px 14px;
font: inherit;
}
input {
border: 1px solid #b8c2d1;
}
button {
border: 0;
color: #ffffff;
background: #6d28d9;
cursor: pointer;
font-weight: 700;
}
button:hover {
background: #5b21b6;
}
Why Separate the Input and Button Styles?
The shared block keeps both controls aligned. Their separate blocks provide the input border plus the button's interactive colors.
- Save static/style.css.
- Refresh PocketLink in Microsoft Edge.
You should see a purple eyebrow above the title. The form should show a bordered input plus a bold purple button.
- Select the existing .result block in static/style.css.
- Replace the selected block by copying these message plus result styles:
.message,
.result {
margin-top: 22px;
padding: 16px;
border-radius: 10px;
}
.error {
color: #991b1b;
background: #fee2e2;
}
.result {
background: #ede9fe;
overflow-wrap: anywhere;
}
.result h2 {
margin-top: 0;
}
.secondary-link {
display: inline-block;
margin-top: 12px;
}
How Do the Feedback Styles Work?
Messages plus successful results share spacing and rounded corners. The .error class gives rejected input a distinct red treatment.
The .secondary-link rule prepares the return link on the missing-code page. Both responses now fit the same card design.
- Save static/style.css.
- Refresh PocketLink in Microsoft Edge.
- Enter example.com in the Long URL field.
- Select Shorten URL.
Good progress. The full-URL message now appears inside a red feedback panel without creating a short link.
Does the Error Look Unstyled?
Confirm that the paragraph has both message plus error classes. Check that .message shares its spacing block with .result.
Help me fix the validation message styling.
Explain unknown short codes
An unknown code has no row in the database. A dedicated template can explain that missing destination while preserving the correct 404 response status.
- In the Visual Studio Code Explorer sidebar, right-click the templates folder.
- Choose the context-menu option for creating a file.
- Enter not_found.html as the file name.
- Build the missing-code page by copying this markup into the new file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Link not found | PocketLink</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="card">
<p class="eyebrow">404</p>
<h1>Short link not found</h1>
<p>No saved destination exists for <code>{{ code }}</code>.</p>
<a class="secondary-link" href="{{ url_for('index') }}">Create a new short link</a>
</main>
</body>
</html>
What Does This Page Explain?
- The {{ code }} expression shows which short code had no saved destination.
- The return link uses the existing index endpoint to send the visitor back to the form.
- The shared stylesheet keeps the missing-code response consistent with the home page.
- Save templates/not_found.html.
- Confirm not_found.html appears inside the templates folder in the Explorer sidebar.
- Switch back to app.py.
- Find the return line beneath if link is None: in follow_link().
- Replace only that return line with this version:
return render_template("not_found.html", code=code), 404
How Does the Route Use the Template?
The route passes the missing code into not_found.html. The final 404 preserves the response status that identifies a missing resource.
- Save app.py.
- Confirm the integrated terminal reports a successful server reload.
Does the Server Report a Template Problem?
Confirm that not_found.html sits inside the existing templates folder. Check that the file name matches the string passed to render_template().
Help me fix my missing-code template.
✔️ Awesome, I've got everything!
Great. Save each PocketLink file before completing the browser checks below.
ⓧ I'd like to double check the full code
Compare each file with its complete version below. Keep pocketlink.sqlite in the pocketlink folder.
Flask==3.1.3
What To Compare in requirements.txt
The dependency file should still contain the single Flask==3.1.3 pin. This step does not add another package.
import secrets
import sqlite3
from urllib.parse import urlsplit
from flask import Flask
from flask import current_app, g
from flask import redirect, url_for
from flask import render_template
from flask import request
app = Flask(__name__)
app.config["DATABASE"] = "pocketlink.sqlite"
def get_db():
if "db" not in g:
g.db = sqlite3.connect(current_app.config["DATABASE"])
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception=None):
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS links (
code TEXT PRIMARY KEY,
original_url TEXT NOT NULL
)
"""
)
db.commit()
with app.app_context():
init_db()
def is_valid_url(value):
try:
parsed = urlsplit(value)
except ValueError:
return False
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
def create_code():
db = get_db()
while True:
code = secrets.token_hex(3)
existing = db.execute(
"SELECT 1 FROM links WHERE code = ?", (code,)
).fetchone()
if existing is None:
return code
@app.route("/", methods=["GET", "POST"])
def index():
error = None
short_url = None
original_url = ""
if request.method == "POST":
original_url = request.form["url"].strip()
if not is_valid_url(original_url):
error = "Enter a full URL starting with http:// or https://."
else:
code = create_code()
db = get_db()
db.execute(
"INSERT INTO links (code, original_url) VALUES (?, ?)",
(code, original_url),
)
db.commit()
short_url = url_for("follow_link", code=code, _external=True)
return render_template(
"index.html",
error=error,
short_url=short_url,
original_url=original_url,
)
@app.route("/<code>")
def follow_link(code):
link = get_db().execute(
"SELECT original_url FROM links WHERE code = ?", (code,)
).fetchone()
if link is None:
return render_template("not_found.html", code=code), 404
return redirect(link["original_url"])
What To Compare in app.py
Confirm the parser import plus is_valid_url() appear before code generation. Check that index() stores valid input only.
Confirm follow_link() renders the missing-code template with a 404 status.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PocketLink</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="card">
<p class="eyebrow">Local Flask project</p>
<h1>PocketLink</h1>
<p class="intro">Paste a long URL and get a compact local link that redirects to it.</p>
<form method="post">
<label for="url">Long URL</label>
<input
id="url"
name="url"
type="text"
value="{{ original_url }}"
placeholder="https://example.com/a/very/long/path"
required
>
<button type="submit">Shorten URL</button>
</form>
{% if error %}
<p class="message error" role="alert">{{ error }}</p>
{% endif %}
{% if short_url %}
<section class="result" aria-live="polite">
<h2>Your short link</h2>
<a href="{{ short_url }}">{{ short_url }}</a>
<p>Destination: {{ original_url }}</p>
</section>
{% endif %}
</main>
</body>
</html>
What To Compare in index.html
Confirm the error condition appears between the form plus the result section. Check that the input markup spans separate lines.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Link not found | PocketLink</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<main class="card">
<p class="eyebrow">404</p>
<h1>Short link not found</h1>
<p>No saved destination exists for <code>{{ code }}</code>.</p>
<a class="secondary-link" href="{{ url_for('index') }}">Create a new short link</a>
</main>
</body>
</html>
What To Compare in not_found.html
Confirm the template displays the missing code. Check that its return link points to the existing home endpoint.
:root {
font-family: "Segoe UI", sans-serif;
color: #172033;
background: #edf2f7;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
display: grid;
place-items: center;
padding: 24px;
}
.card {
width: min(680px, 100%);
background: #ffffff;
border-radius: 18px;
padding: 32px;
box-shadow: 0 18px 45px rgba(23, 32, 51, 0.12);
}
.eyebrow {
margin: 0;
color: #5b21b6;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
h1 {
margin: 8px 0;
font-size: clamp(2rem, 8vw, 3.5rem);
}
.intro {
color: #526077;
}
form {
display: grid;
gap: 12px;
margin-top: 28px;
}
label {
font-weight: 700;
}
input,
button {
width: 100%;
border-radius: 10px;
padding: 13px 14px;
font: inherit;
}
input {
border: 1px solid #b8c2d1;
}
button {
border: 0;
color: #ffffff;
background: #6d28d9;
cursor: pointer;
font-weight: 700;
}
button:hover {
background: #5b21b6;
}
.message,
.result {
margin-top: 22px;
padding: 16px;
border-radius: 10px;
}
.error {
color: #991b1b;
background: #fee2e2;
}
.result {
background: #ede9fe;
overflow-wrap: anywhere;
}
.result h2 {
margin-top: 0;
}
.secondary-link {
display: inline-block;
margin-top: 12px;
}
What To Compare in style.css
Confirm the typography plus form-control blocks appear before the feedback styles. Check that .error plus .secondary-link appear near the end.
Before you test, predict what PocketLink does with example.com. Predict what it does with /missing.
- Return to the PocketLink home page in Microsoft Edge.
- Enter example.com in the Long URL field.
- Select Shorten URL.
You should see the full-URL error inside the red panel. PocketLink does not generate a short link for this input.
- Enter http://127.0.0.1:5000/missing in the Microsoft Edge address bar.
- Load the address.
You should see the Short link not found page. It should identify missing as the unknown code.
- Use Microsoft Edge history to reopen the valid short link from the previous step.
You should reach its saved destination. That closes the safety gap. PocketLink now protects its database plus explains missing links.
Does a Valid Link Show the Missing Page?
Confirm that the code in the address matches a row created earlier. Check that the database query still uses SELECT original_url FROM links WHERE code = ?.
Help me diagnose a valid short link.
Secret mission
Count Every Visit
Turn each successful redirect into visible traffic data. Extend PocketLink with a saved-links table that shows every short code and its click count.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep PocketLink running, pause it for another session, or delete it completely. This project runs entirely on your computer, so there are no ongoing costs.
Resources you used:
- One local Flask development server running PocketLink for browser testing.
- The pocketlink folder containing app.py, requirements.txt, the templates folder, the static folder, the .venv environment, and the pocketlink.sqlite database.
Keep everything running
No action needed. Choose this if you want to keep testing short links or watching their click counts change.
- Leave the Flask development server running in the integrated Command Prompt terminal.
- Keep the pocketlink folder so your source files, virtual environment, saved links, and click counts remain available.
- Keep Microsoft Edge open at http://127.0.0.1:5000/ while you continue testing PocketLink.
Pause - I'll come back to this later
Stop the local server to free its port. Your project files and saved click data remain ready for another session.
- Return to the integrated Command Prompt terminal in Visual Studio Code.
- Press Ctrl+C to stop Flask.
- Refresh http://127.0.0.1:5000/ in Microsoft Edge.
PocketLink no longer loads because the local server has stopped. Your pocketlink folder still contains the app, its virtual environment, and every saved click count.
Delete - I don't want to use this again
Deleting the folder removes your local PocketLink project. Save anything you want to keep before continuing.
- Return to the integrated Command Prompt terminal in Visual Studio Code.
- Press Ctrl+C to stop Flask.
- Close Visual Studio Code.
- Use the Windows file browser to return to the parent folder where you created pocketlink.
- Delete the pocketlink folder.
- Confirm that pocketlink no longer appears in its parent folder.
The source files, .venv environment, and SQLite database are now removed from their original location.
Nice Work!
Nice Work!
You did it! Your local Flask app turns long web addresses into persistent short links.
What you learned:
- Build a browser-based URL submission form with Jinja templates. Reject malformed destinations with clear feedback.
- Generate compact short codes. Connect each code to a dynamic redirect route. Return a useful 404 response for unknown links.
- Expose the limits of in-memory storage through a restart test. Preserve mappings with SQLite. Protect database operations with parameterized SQL. Save each change with an explicit commit.
- Secret Mission: Extend the database schema with visible click counts. Record every successful redirect. Display the most visited links first.
Ready to quiz yourself?