Build a URL Shortener API

Build a mini bit.ly with FastAPI, SQLite, and Base62 encoding.

Introduction

30 Second Summary

Every time you click a shortened link on social media or in a text message, a system instantly translates that tiny code back into the full web address. It's one of the most common pieces of internet infrastructure, and one of the most popular system design interview questions.

In this project, you will build your own URL shortener API from scratch. You'll design the database schema, implement the encoding algorithm that production shorteners actually use, and create the endpoints that shorten URLs and redirect visitors.

What You'll Build

A fully functional URL shortener API (like a mini bit.ly) running on your machine, with interactive documentation that lets you test every endpoint from your browser.

By the end of this project, you'll have:

  • A FastAPI backend with two core endpoints for shortening URLs and redirecting visitors.
  • A SQLite database with a schema designed around the dominant query pattern (short code lookup).
  • A custom Base62 encoder that converts database IDs into short, URL-safe codes the same way production shorteners do.
  • Secret Mission: Add click tracking analytics with a separate table to count and timestamp every redirect.

Are there any prerequisites?

You should be comfortable with basic Python concepts like variables, functions, and loops. No web development or database experience is needed.

Set Up Your Project

Every API project starts with a clean environment where you can install packages without conflicts. Python's virtual environments keep your URL shortener dependencies isolated from the rest of your system.

In this step, you'll create a project folder, install FastAPI, and confirm the development server runs with auto-generated documentation in your browser.

In this step, get ready to:
  • Create a project folder with a Python virtual environment.
  • Install FastAPI and run the development server.
  • Verify the auto-generated API docs load at /docs.
Create a project folder and virtual environment

A virtual environment is an isolated space where Python installs packages just for this project. Without one, packages from different projects can conflict with each other.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) 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
  • Create and activate a virtual environment by running the commands for your operating system:

macOS

python3 -m venv venv
source venv/bin/activate

Windows

python -m venv venv
venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran both commands (create and activate) in order. On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned first, then try the activate command again.

help me activate my Python virtual environment

Install FastAPI and verify the dev server runs
  • Install FastAPI and its dependencies by running:
pip install "fastapi[standard]"

What does this command do?

The fastapi[standard] package installs FastAPI plus everything you need for development, including Uvicorn (the server that runs your API) and the FastAPI CLI.

Seeing a 'pip not found' error?

Make sure your virtual environment is active. You should see (venv) in your prompt. If not, re-run the activate command from the previous substep.

help me fix pip not found in my virtual environment

Now you need a Python file for FastAPI to serve. You'll create this in VS Code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the url-shortener folder, then click Open.
  • In the Explorer sidebar on the left, click the new file icon (a page with a plus sign).
  • Name the file main.py and press Enter.
  • Copy the following starter code into main.py:
from fastapi import FastAPI

app = FastAPI(title="URL Shortener", version="1.0.0")

What does this code do?

  • from fastapi import FastAPI imports the FastAPI class from the package you just installed.
  • app = FastAPI(...) creates your application instance. The title and version parameters show up in the auto-generated docs.
  • Save main.py with Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI(title="URL Shortener", version="1.0.0")

Now start the development server to confirm everything works.

  • Open VS Code's integrated terminal by pressing Ctrl+` (backtick key, same shortcut on macOS and Windows).
  • If your virtual environment is not active in this terminal (you don't see (venv) in the prompt), re-run the activate command for your OS from earlier.
  • Start the FastAPI development server by running:
fastapi dev

What does this command do?

The fastapi dev command auto-detects your main.py file and starts a local server with auto-reload enabled. Any time you save a change, the server restarts automatically.

You should see output in the terminal confirming the server is running at http://127.0.0.1:8000.

Seeing 'command not found' for fastapi?

Make sure your virtual environment is active (check for (venv) in the prompt). The fastapi CLI was installed inside the virtual environment, so it's only available when the environment is active.

help me fix fastapi command not found

Confirm the interactive docs load at /docs

FastAPI automatically generates interactive API documentation from your code. This is powered by Swagger UI and lets you test endpoints directly in your browser.

  • Open your browser and navigate to http://127.0.0.1:8000/docs.

You should see a page titled URL Shortener with version 1.0.0 displayed. The page will look mostly empty because you haven't added any endpoints yet.

Why are the docs generated automatically?

FastAPI reads your Python type hints and Pydantic models to build an OpenAPI specification. Swagger UI then renders that specification as an interactive page where you can send requests and see responses.

You'll use this docs page throughout the project to test your API without needing a separate tool like Postman.

Keep the server running

Leave the fastapi dev process running in your terminal for the rest of the project. When you need to run other commands, open a new terminal tab or use a second VS Code integrated terminal.

Your development environment is ready and the server is running. Next up, you'll design the database schema that stores your URL mappings.

Design Your Database Schema

Your development environment is ready and the server is confirmed working. Now it's time to give your URL shortener a memory.

Every URL shortener needs a way to remember which short code maps to which long URL. In this step, you'll design and build the database layer using SQLite. SQLite ships with Python's standard library, so there's nothing extra to install.

In this step, get ready to:
  • Create a database connection helper and define the URL table schema.
  • Write functions to insert and look up URLs.
  • Understand why the schema is optimized for the dominant query pattern.
Create the database connection and table schema

Before writing any API logic, you need a place to store URL mappings. You'll create a dedicated database.py file that handles all database operations.

  • In VS Code, create a new file called database.py in your url-shortener folder.
  • Add the database connection and table creation code by pasting the following into database.py:
import sqlite3
from datetime import datetime, timezone


def get_connection():
    # Connect to the SQLite database file (creates it if it doesn't exist)
    conn = sqlite3.connect("urls.db")
    # Return rows as dictionary-like objects instead of plain tuples
    conn.row_factory = sqlite3.Row
    return conn


def create_tables():
    # Set up the urls table to store all short code mappings
    conn = get_connection()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS urls (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            short_code TEXT UNIQUE NOT NULL,
            long_url TEXT NOT NULL,
            created_at TEXT NOT NULL
        )
    """)
    conn.commit()
    conn.close()

What does this code do?

  • get_connection() opens (or creates) a file called urls.db and sets row_factory = sqlite3.Row so query results behave like dictionaries instead of plain tuples.
  • create_tables() defines the urls table with four columns: an auto-incrementing id, a short_code that must be unique, the original long_url, and a created_at timestamp.
  • CREATE TABLE IF NOT EXISTS means running the file multiple times won't crash or duplicate the table.

Why is short_code marked UNIQUE?

The most common operation your shortener performs is: given a short code, find the original URL. In database terms, that's SELECT long_url FROM urls WHERE short_code = ?. By marking short_code as UNIQUE, SQLite automatically creates an index on that column.

This means lookups by short code are fast even with millions of rows. Designing your schema around the dominant query pattern is a core system design principle.

You won't see a change yet. The next chunk adds the helper functions that read and write data.

Write the helper functions

Your table exists, but you need functions to actually put data in and get data out. You'll write two helpers: one to insert a new URL mapping, and one to look up an existing mapping by its short code.

  • Add the insert function below your create_tables() function in database.py:
def insert_url(long_url: str, short_code: str) -> int:
    # Insert a new URL mapping and return its database ID
    conn = get_connection()
    cursor = conn.execute(
        "INSERT INTO urls (long_url, short_code, created_at) VALUES (?, ?, ?)",
        (long_url, short_code, datetime.now(timezone.utc).isoformat()),
    )
    conn.commit()
    url_id = cursor.lastrowid
    conn.close()
    return url_id

What does this code do?

  • insert_url() takes a long URL and a short code, inserts them into the urls table with the current UTC timestamp, and returns the new row's id.
  • The ? placeholders prevent SQL injection by letting SQLite handle value escaping.
  • cursor.lastrowid gives you the auto-generated ID, which you'll use later to create the Base62-encoded short code.

You won't see a change yet. The next function completes the database layer.

  • Add the lookup function and the module-level create_tables() call below your insert_url() function:
def get_url_by_short_code(short_code: str) -> dict | None:
    # Look up a URL mapping by its short code
    conn = get_connection()
    row = conn.execute(
        "SELECT id, short_code, long_url, created_at FROM urls WHERE short_code = ?",
        (short_code,),
    ).fetchone()
    conn.close()
    if row:
        return dict(row)
    return None


# Create the table as soon as this module is imported
create_tables()

What does this code do?

  • get_url_by_short_code() queries the database for a row matching the given short code. It returns a dictionary if found, or None if the code doesn't exist.
  • The create_tables() call at the bottom runs automatically whenever another file imports database.py. This guarantees the table exists before any queries run.
  • Save database.py.
  • Confirm the database gets created by running this command in your terminal:
python database.py
  • Verify the file was created by running:
ls

You should see urls.db in the output alongside database.py and main.py.

Don't see urls.db?

  • Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
  • Check that you saved database.py before running the command. Unsaved changes won't execute.
  • Make sure you're running the command from inside the url-shortener folder. Run pwd to check your current location.

Still stuck? Help me figure out why urls.db isn't being created when I run database.py

✔️ Awesome, I've got everything!

Great. Double check you've saved database.py and confirmed urls.db exists in your project folder.

ⓧ I'd like to double check the full code

Here's the complete database.py file for comparison:

import sqlite3
from datetime import datetime, timezone


def get_connection():
    conn = sqlite3.connect("urls.db")
    conn.row_factory = sqlite3.Row
    return conn


def create_tables():
    conn = get_connection()
    conn.execute("""
        CREATE TABLE IF NOT EXISTS urls (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            short_code TEXT UNIQUE NOT NULL,
            long_url TEXT NOT NULL,
            created_at TEXT NOT NULL
        )
    """)
    conn.commit()
    conn.close()


def insert_url(long_url: str, short_code: str) -> int:
    conn = get_connection()
    cursor = conn.execute(
        "INSERT INTO urls (long_url, short_code, created_at) VALUES (?, ?, ?)",
        (long_url, short_code, datetime.now(timezone.utc).isoformat()),
    )
    conn.commit()
    url_id = cursor.lastrowid
    conn.close()
    return url_id


def get_url_by_short_code(short_code: str) -> dict | None:
    conn = get_connection()
    row = conn.execute(
        "SELECT id, short_code, long_url, created_at FROM urls WHERE short_code = ?",
        (short_code,),
    ).fetchone()
    conn.close()
    if row:
        return dict(row)
    return None


create_tables()

Your database layer is complete. Next up, you'll build the Base62 encoder that turns those auto-incrementing IDs into short, URL-safe codes.

Implement Base62 Encoding

Your database is ready to store URL mappings. Now you need a way to generate the short codes themselves.

Every URL you shorten gets a database ID (1, 2, 3, and so on). You need an algorithm that converts those IDs into short, URL-safe strings like 1C or 4c92. That algorithm is Base62 encoding.

In this step, get ready to:
  • Create an encoder module with a Base62 encode function.
  • Test the encoder to confirm it produces short alphanumeric codes.
  • Understand why Base62 beats hashing for collision-free short code generation.
Create the Base62 encoder

Base62 encoding works like counting in a number system with 62 digits instead of 10. It uses all digits (0-9), lowercase letters (a-z), and uppercase letters (A-Z). Every character is URL-safe, so the output can go directly into a web address.

  • Create a new file called encoder.py in your url-shortener folder.
  • Add the following code to encoder.py:
BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"


def encode_base62(number: int) -> str:
    # Handle the edge case where the input is zero
    if number == 0:
        return BASE62_ALPHABET[0]

    # Build the encoded string by repeatedly dividing by 62
    result = []
    while number > 0:
        remainder = number % 62
        result.append(BASE62_ALPHABET[remainder])
        number //= 62

    # Reverse because we built the string from least-significant digit first
    return "".join(reversed(result))

What does this code do?

BASE62_ALPHABET defines the 62 characters available for short codes. The order matters because each character's position is its numeric value.

The encode_base62() function works like converting a decimal number to a different base. It repeatedly divides the number by 62, using each remainder as an index into the alphabet. The result is a short string that uniquely represents the original number.

  • Save encoder.py.

Seeing a syntax error?

  • Check that the quotes around the alphabet string are straight double quotes, not curly quotes.
  • Make sure the def line has a colon at the end.
  • Confirm that //= uses two forward slashes (integer division), not one.

Help me fix a syntax error in my encoder.py file

Test the encoder in your terminal

Before wiring the encoder into your API, you should verify it works on its own. You can test it directly from the terminal by running a quick Python command.

  • Test your encoder with a few sample IDs by running this command:
python3 -c "from encoder import encode_base62; print(encode_base62(1)); print(encode_base62(100)); print(encode_base62(1000000))"

You should see three short alphanumeric strings printed, one per line. ID 1 outputs 1, ID 100 outputs 1C, and ID 1000000 outputs 4c92. Notice how even a million URLs only needs a 4-character code.

Seeing ModuleNotFoundError?

  • Make sure you run the command from inside your url-shortener folder (the same folder where encoder.py lives).
  • Confirm the file is named exactly encoder.py (not encode.py or encoders.py).

Help me fix a ModuleNotFoundError when importing encoder

Why Base62 instead of hashing?

You might wonder why not just hash the URL with MD5 or SHA and truncate it. The problem is that truncated hashes can collide. Two different URLs could produce the same short code.

Base62 encoding of auto-increment IDs guarantees zero collisions. Every database ID maps to exactly one unique string, and every string maps back to exactly one ID. This is the approach production URL shorteners actually use.

How short can codes get?

With 62 characters, a 6-character code gives you roughly 56 billion unique combinations. That is more than enough for most URL shortening services. Your encoder automatically uses the minimum number of characters needed for each ID.

✔️ Awesome, I've got everything!

Great. Double check you've saved encoder.py and that your test command printed the expected output.

ⓧ I'd like to double check the full code

Here is the complete encoder.py file:

BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"


def encode_base62(number: int) -> str:
    if number == 0:
        return BASE62_ALPHABET[0]

    result = []
    while number > 0:
        remainder = number % 62
        result.append(BASE62_ALPHABET[remainder])
        number //= 62

    return "".join(reversed(result))

Your encoder is ready to generate short codes from any database ID. Next up, you'll wire everything together into actual API endpoints that accept URLs and return short links.

Build the API Endpoints

Your database layer stores URLs and your encoder turns IDs into short codes. Now it's time to connect everything through an API that the outside world can actually talk to.

In this step, you'll build two endpoints that form the core of any URL shortener. A POST endpoint accepts a long URL and returns a short code. A GET endpoint accepts a short code and redirects the browser to the original URL.

In this step, get ready to:
  • Define Pydantic models for request and response validation.
  • Create a POST /shorten endpoint that shortens URLs.
  • Create a GET /{short_code} endpoint that redirects to the original URL.
Set up imports and Pydantic models

Before writing the endpoints, you need to import the right tools and define the shape of your API's input and output. FastAPI uses Pydantic models to automatically validate incoming data and generate documentation.

  • In main.py, replace the existing content with the following code:
from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse
from pydantic import BaseModel

from database import insert_url, get_url_by_short_code
from encoder import encode_base62

# Create the FastAPI application instance
app = FastAPI(title="URL Shortener", version="1.0.0")

# Base URL for constructing full short URLs in responses
BASE_URL = "http://127.0.0.1:8000"


# Pydantic model defining what the client sends (a long URL)
class ShortenRequest(BaseModel):
    url: str


# Pydantic model defining what the server sends back
class ShortenResponse(BaseModel):
    short_url: str
    short_code: str

What does this code do?

  • HTTPException lets you return proper HTTP error codes (like 404) when something goes wrong.
  • RedirectResponse sends the browser to a different URL instead of returning data.
  • ShortenRequest tells FastAPI that incoming POST requests must include a url field. If it's missing, FastAPI automatically returns a validation error.
  • ShortenResponse defines exactly what your API returns: a short_url (the full clickable link) and a short_code (just the code portion).

What is Pydantic?

Pydantic is a data validation library that FastAPI uses under the hood. When you define a class like ShortenRequest(BaseModel), you're declaring a contract: requests to this endpoint MUST include these fields with these types.

FastAPI uses these models for two things: rejecting invalid requests before your code runs, and generating the interactive documentation at /docs.

  • Save main.py.

You won't see a visible change yet. The next chunk adds the POST endpoint that puts these models to work.

Server showing an error in the terminal?

  • Check that your database.py and encoder.py files are in the same folder as main.py. The from database import ... line needs them in the same directory.
  • Make sure you replaced the entire file content, including the from fastapi import FastAPI line at the top. The new imports add HTTPException which the old line didn't have.
  • help me debug an import error in my FastAPI main.py file
Create the POST /shorten endpoint

This endpoint is the heart of your URL shortener. It receives a long URL, stores it in the database, generates a short code, and returns the result.

  • Add the following function below the ShortenResponse class in main.py:
@app.post("/shorten", response_model=ShortenResponse)
def shorten_url(request: ShortenRequest):
    # Insert with a temporary placeholder (we need the ID to generate the code)
    placeholder_code = "temp"
    url_id = insert_url(request.url, placeholder_code)

    # Encode the database ID into a Base62 short code
    short_code = encode_base62(url_id)

    # Update the database row with the real short code
    from database import get_connection

    conn = get_connection()
    conn.execute(
        "UPDATE urls SET short_code = ? WHERE id = ?",
        (short_code, url_id),
    )
    conn.commit()
    conn.close()

    # Return the full short URL and code to the client
    return ShortenResponse(
        short_url=f"{BASE_URL}/{short_code}",
        short_code=short_code,
    )

What does this code do?

  • The @app.post("/shorten") decorator registers this function as a POST endpoint at the /shorten path.
  • It inserts the URL with a placeholder short_code of "temp" first because you need the auto-increment id before you can generate the Base62 code.
  • Once the ID is known, encode_base62(url_id) converts it to a short alphanumeric string.
  • The UPDATE query replaces "temp" with the real short code in the database row.

Why the placeholder approach?

Your database schema requires a short_code value at insert time (it's NOT NULL). But the short code is derived from the database ID, which only exists after the insert.

The insert-then-update pattern solves this chicken-and-egg problem. Production systems use pre-allocated ID ranges to avoid the extra query, but for a learning project this approach is clear and correct.

  • Save main.py.
  • Refresh http://127.0.0.1:8000/docs in your browser. You should see a blue POST /shorten entry in the endpoint list.

Don't see the POST endpoint?

  • Make sure you saved main.py. The fastapi dev server auto-reloads on save.
  • Check your terminal for error messages. A red traceback usually points to a syntax error or indent problem in the code you just added.
  • Verify the @app.post decorator is directly above the def shorten_url line with no blank lines between them.
  • help me fix my POST /shorten endpoint not showing up in FastAPI docs
Create the GET /{short_code} endpoint

The redirect endpoint completes the loop. When someone visits a short URL, this endpoint looks up the original URL and sends the browser there.

  • Add the following function below the shorten_url function in main.py:
@app.get("/{short_code}")
def redirect_to_url(short_code: str):
    # Look up the original URL from the database
    url_data = get_url_by_short_code(short_code)
    if not url_data:
        raise HTTPException(status_code=404, detail="Short URL not found")
    # Redirect the browser to the original long URL
    return RedirectResponse(url=url_data["long_url"], status_code=302)

What does this code do?

  • The @app.get("/{short_code}") decorator captures any path after / and passes it as the short_code parameter.
  • get_url_by_short_code queries your SQLite database for a matching row.
  • If no match exists, HTTPException(status_code=404) returns a proper "not found" error.
  • If found, RedirectResponse tells the browser to navigate to the original long URL.

Why 302 instead of 301?

HTTP status 302 means "temporary redirect." The browser does not cache the redirect, so every click hits your server. This is essential for accurate click tracking (which you'll add in the Secret Mission).

HTTP status 301 means "permanent redirect." Browsers cache 301 redirects, so future visits skip your server entirely. Production shorteners like bit.ly default to 302 for this reason.

  • Save main.py.
  • Refresh http://127.0.0.1:8000/docs in your browser. You should now see both POST /shorten and GET /{short_code} listed.

Seeing an error instead of two endpoints?

  • Look at the terminal running fastapi dev. If you see an IndentationError, double-check that every line inside redirect_to_url uses 4 spaces (not tabs).
  • Make sure raise HTTPException(...) is inside the if not url_data: block (indented one extra level).
  • help me debug my GET redirect endpoint in FastAPI

✔️ Awesome, I've got everything!

Great. Double check you've saved main.py and both endpoints appear in your /docs page.

ⓧ I'd like to double check the full code

Here is the complete main.py file at this point:

from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse
from pydantic import BaseModel

from database import insert_url, get_url_by_short_code
from encoder import encode_base62

app = FastAPI(title="URL Shortener", version="1.0.0")

BASE_URL = "http://127.0.0.1:8000"


class ShortenRequest(BaseModel):
    url: str


class ShortenResponse(BaseModel):
    short_url: str
    short_code: str


@app.post("/shorten", response_model=ShortenResponse)
def shorten_url(request: ShortenRequest):
    placeholder_code = "temp"
    url_id = insert_url(request.url, placeholder_code)

    short_code = encode_base62(url_id)

    from database import get_connection

    conn = get_connection()
    conn.execute(
        "UPDATE urls SET short_code = ? WHERE id = ?",
        (short_code, url_id),
    )
    conn.commit()
    conn.close()

    return ShortenResponse(
        short_url=f"{BASE_URL}/{short_code}",
        short_code=short_code,
    )


@app.get("/{short_code}")
def redirect_to_url(short_code: str):
    url_data = get_url_by_short_code(short_code)
    if not url_data:
        raise HTTPException(status_code=404, detail="Short URL not found")
    return RedirectResponse(url=url_data["long_url"], status_code=302)

Your API is built. You have a POST endpoint that shortens URLs and a GET endpoint that redirects. Next up, you'll put them to the test by creating short URLs and watching the redirects happen in real time.

Test Your URL Shortener

You now have a complete URL shortener with a database, an encoder, and two API endpoints.

In this step, you will start the server and run it through its paces end-to-end. You will shorten URLs, follow redirects, and confirm that error handling works correctly.

In this step, get ready to:
  • Start the dev server and open the interactive Swagger UI docs.
  • Create short URLs and verify the POST endpoint returns the correct response.
  • Test redirects and 404 error handling through your browser.
Start the dev server and open Swagger UI

FastAPI ships with auto-generated interactive documentation. This means you can test every endpoint directly in your browser without needing a separate tool like Postman.

  • In your terminal, make sure you are inside your url-shortener folder with your virtual environment activated.
  • Start the FastAPI dev server by running this command:
fastapi dev

What does this command do?

fastapi dev auto-detects your main.py file and starts a Uvicorn server with auto-reload enabled. Any changes you save to your files will restart the server automatically.

  • You should see output in your terminal indicating the server is running at http://127.0.0.1:8000.
  • Open your browser and navigate to http://127.0.0.1:8000/docs.

You should see the Swagger UI page titled "URL Shortener" with your two endpoints listed: POST /shorten and GET /{short_code}.

Don't see the docs page?

  • Check your terminal for error messages. A common cause is a syntax error in one of your Python files.
  • Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • Confirm you are running the command from inside the url-shortener folder where main.py lives.
  • My FastAPI server won't start. How do I debug this?
Create short URLs via the POST endpoint

The Swagger UI lets you send real requests to your API and see the responses instantly. You will use it to shorten your first URL.

  • In the Swagger UI page, click on the green POST /shorten bar to expand it.
  • Click the Try it out button on the right side.
  • In the Request body editor, replace the default content with this JSON:
{
  "url": "https://www.wikipedia.org/wiki/URL_shortening"
}
  • Click the blue Execute button.

Scroll down to the Server response section. You should see a 200 response code with a JSON body containing short_url and short_code.

What should the response look like?

Your response body will look something like this (the exact short_code depends on your database ID):

{
  "short_url": "http://127.0.0.1:8000/1",
  "short_code": "1"
}

The first URL you shorten gets database ID 1, which encodes to 1 in Base62. The second will be 2, and so on. Once you reach ID 62, the codes start using two characters.

  • Create a second short URL by scrolling back up to the Request body and changing the URL to something different, like https://github.com.
  • Click Execute again.

You should see a new response with a different short_code value. Each URL gets its own unique code because each gets a unique database ID.

Seeing a 500 Internal Server Error?

  • Check your terminal for the full error traceback. The most common cause is a typo in database.py or main.py.
  • If you see a UNIQUE constraint error, delete your urls.db file and restart the server. The table will be recreated automatically.
  • My POST /shorten endpoint returns a 500 error. Help me debug.
Test redirects and verify 404 handling

Creating short URLs is only half the story. The real test is whether visiting a short URL actually sends you to the original destination.

  • Open a new browser tab.
  • In the address bar, type http://127.0.0.1:8000/1 and press Enter.

Your browser should redirect you to the original long URL you shortened earlier (the Wikipedia page). This is your 302 redirect in action.

Why 302 and not 301?

A 302 (Temporary Redirect) means the browser does not cache the redirect. Every click hits your server, which lets you track analytics accurately.

A 301 (Permanent Redirect) tells browsers to cache the destination. Future visits skip your server entirely, making click tracking impossible. Production shorteners like bit.ly default to 302 for this reason.

  • Try your second short URL too. Type http://127.0.0.1:8000/2 in the address bar and press Enter.

You should land on the second URL you shortened (GitHub).

Now test what happens when someone visits a short code that does not exist in your database.

  • In your browser address bar, type http://127.0.0.1:8000/zzz and press Enter.

You should see a JSON response with a 404 status and the detail message Short URL not found. This confirms your error handling is working correctly.

Why does good error handling matter?

In a system design interview, showing that your API returns proper HTTP status codes (200 for success, 404 for not found, 500 for server errors) demonstrates you understand the REST API contract between client and server.

A 404 tells the client "this resource does not exist" so it can show a helpful message to the user instead of crashing.

Redirect not working?

  • Make sure you include the full address including http:// at the start. Without it, your browser may try to search instead of navigate.
  • If you see a JSON 404 response instead of a redirect, the short code you typed does not match any record in your database. Double check the short_code value from your POST response.
  • My short URL is not redirecting to the original page. What could be wrong?

Your URL shortener is fully working. You have a complete system that stores URLs, generates short codes, and handles both successful redirects and error cases. Next up is an optional challenge to add click tracking analytics to your shortener.

Secret mission

Add Click Analytics

Your URL shortener works, but you have no idea how popular each link is. In this secret mission, you'll add click tracking analytics with a separate table, learning a key system design principle that separates junior from senior engineers.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your project files for future reference, pause by stopping the dev server, or delete everything entirely.

Resources you used:

  • Project folder on your Desktop containing main.py, database.py, and encoder.py.
  • SQLite database file (urls.db) inside your project folder.
  • Python virtual environment (venv) inside your project folder with FastAPI installed.

Keep everything running

No action needed. Choose this if you want to keep experimenting with your URL shortener or extend it further.

  • Your project folder stays on your Desktop for easy access.
  • To restart the API later, open your terminal, navigate to the project folder, activate the virtual environment, and run fastapi dev.
  • Your urls.db database retains all shortened URLs you created.

Pause - I'll come back to this later

Stop the running dev server to free up the terminal. Your files and database stay intact for when you return.

  • In the terminal where the FastAPI server is running, press Ctrl+C to stop it.
  • Deactivate your virtual environment by running:
deactivate
  • Your project folder and database remain on your Desktop. To resume later, navigate to the folder, activate the virtual environment, and run fastapi dev again.

Delete - I don't want to use this again

Remove all project files, the virtual environment, and the database from your machine.

  • In the terminal where the FastAPI server is running, press Ctrl+C to stop it.
  • Delete the entire project folder by running the command for your operating system.

macOS

  • Navigate to your Desktop and remove the project folder by running:
cd ~/Desktop
rm -rf url-shortener

Windows

  • Navigate to your Desktop and remove the project folder by running:
cd ~\Desktop
rmdir /s /q url-shortener
  • This removes all Python files, the virtual environment (including FastAPI), and the urls.db database.
  • Python and VS Code remain installed on your machine since they were already there before this project.

Nice Work!

Nice Work!

You just built a fully functional URL shortener API from scratch. This is the same system design problem that comes up in technical interviews, and you now have a working implementation you can point to.

You've learned how to:

  • Build a working REST API with FastAPI, including interactive Swagger UI documentation that lets you test endpoints in your browser.
  • Design a database schema optimized for the dominant read query, using SQLite to store URL mappings with indexed short code lookups.
  • Implement Base62 encoding, the same collision-free algorithm production URL shorteners use to convert database IDs into short, URL-safe codes.
  • Secret Mission: Added click analytics with a separate table, learning the system design principle of separating concerns by access pattern.

Ready to quiz yourself?