The problem
Long URLs are inconvenient to share, difficult to remember, and often contain tracking parameters or complex paths.
A URL shortener solves this by converting a long URL into a short identifier:
https://example.com/products/artificial-intelligence/course?campaign=summer
↓
https://short.ly/aB7xK2
When someone opens the short URL, the service looks up the identifier and redirects the user to the original address.
At first, this looks like a very small project.
It is.
That is exactly why it is useful.
You can start with a simple CRUD-style API and then progressively introduce the engineering problems that appear in real backend systems:
- database design
- unique identifiers
- validation
- authentication
- caching
- rate limiting
- analytics
- testing
- observability
- deployment
- scalability
What you are building
The final application should allow a user to:
- Submit a long URL.
- Receive a unique short URL.
- Open the short URL and be redirected.
- View basic click statistics.
- Create and manage their links.
- Disable a link when necessary.
A basic interaction looks like:
POST /api/urls
↓
Generate Short Code
↓
Store URL
↓
Return Short URL
When someone visits it:
GET /aB7xK2
↓
Check Cache
↓
Check Database
↓
Record Visit
↓
HTTP Redirect
Why this is a strong software project
This project does not depend on artificial intelligence.
That is intentional.
A strong AI engineer still needs strong software engineering fundamentals.
This project gives you practice with:
- HTTP
- REST
- API design
- database schemas
- indexes
- transactions
- caching
- authentication
- testing
- Docker
- CI/CD
- deployment
- system design
These skills transfer directly to AI systems later.
Architecture
Start with a simple architecture:
Client
↓
FastAPI
┌─────┴─────┐
↓ ↓
Cache PostgreSQL
│ │
└─────┬─────┘
↓
Redirect
A more complete version can become:
┌───────────────┐
│ Client │
└───────┬───────┘
↓
┌───────────────┐
│ Reverse Proxy │
└───────┬───────┘
↓
┌───────────────┐
│ FastAPI │
└───────┬───────┘
│
┌─────────────┼─────────────┐
↓ ↓ ↓
PostgreSQL Redis Analytics
│ │ │
└─────────────┼─────────────┘
↓
Response
Do not build the complete architecture immediately.
Add complexity only when the simpler system works.
Step 1 — Design the API
Start by defining the API before writing implementation code.
Example endpoints:
POST /api/urls
GET /api/urls/{id}
GET /api/urls/{id}/stats
DELETE /api/urls/{id}
GET /{short_code}
A request to create a URL might be:
{
"url": "https://example.com/articles/software-engineering"
}
The response could be:
{
"id": "42",
"short_code": "aB7xK2",
"short_url": "https://short.example/aB7xK2",
"url": "https://example.com/articles/software-engineering"
}
Keep the API small at first.
Step 2 — Validate URLs
Never trust user input.
Validate:
- scheme
- hostname
- URL format
- maximum length
- unsupported protocols
For example, your service should normally accept:
https://example.com
http://example.com
and reject malformed input.
Use application-level validation before writing anything to the database.
Step 3 — Design the database
A minimal URL table could contain:
urls
──────────────────────────────
id
short_code
original_url
created_at
updated_at
expires_at
user_id
is_active
Important constraints include:
short_code → UNIQUE
Depending on your product requirements, you may also want an index on:
user_id
created_at
expires_at
The database should enforce important invariants instead of relying only on application code.
Step 4 — Generate short codes
You need a way to generate short identifiers.
For example:
aB7xK2
xP4mQ9
K8zT1c
Possible strategies include:
- random strings
- encoded numeric IDs
- hash-based identifiers
Do not use a scheme that can silently produce duplicate values.
Your system should handle collisions safely.
A good design treats uniqueness as a database-level guarantee.
Step 5 — Create the API
Build the first version using FastAPI.
The application should:
Receive URL
↓
Validate
↓
Generate short code
↓
Insert into database
↓
Return response
Keep your route handlers thin.
A useful separation is:
API Route
↓
Service Layer
↓
Repository / Database Layer
This keeps business logic from becoming tangled with HTTP-specific code.
Step 6 — Implement redirects
The redirect endpoint is the most important part of the service:
GET /{short_code}
It should:
- Validate the short code.
- Find the record.
- Check whether the link is active.
- Check expiration.
- Record the visit.
- Redirect to the original URL.
A missing URL should return an appropriate HTTP error rather than a generic server failure.
Step 7 — Add analytics
Track useful information such as:
url_id
timestamp
status
referrer
user_agent
You can later add:
- country
- device type
- browser
- operating system
Be careful about privacy.
Collect only what you actually need and clearly communicate what is being collected.
Step 8 — Add Redis caching
Most successful short URLs may be requested repeatedly.
Without caching:
Request
↓
FastAPI
↓
PostgreSQL
↓
Redirect
With caching:
Request
↓
Redis
├── Hit → Redirect
│
└── Miss
↓
PostgreSQL
↓
Redis SET
↓
Redirect
This changes the performance characteristics of the redirect path.
Start without Redis.
Add it after you have measured the basic system.
Step 9 — Cache carefully
Caching introduces its own engineering problems.
You need to decide:
- cache key
- expiration time
- invalidation strategy
- what happens when a URL is deleted
- what happens when a URL expires
For example:
DELETE /api/urls/aB7xK2
↓
Database disabled
↓
Cache invalidated
Otherwise a deleted link may continue redirecting because stale data remains in the cache.
Step 10 — Add authentication
Create user accounts or API authentication if you want users to manage their links.
An authenticated user should be able to:
Create URLs
List URLs
View analytics
Delete URLs
Manage expiration
The redirect endpoint can remain public.
Keep authentication separate from public link resolution.
Step 11 — Add rate limiting
Public APIs are vulnerable to abuse.
A user could repeatedly call:
POST /api/urls
and generate thousands of links.
Add rate limiting such as:
100 requests / hour / user
The exact value is a product decision.
The important lesson is understanding that public endpoints need abuse protection.
Step 12 — Add URL expiration
Allow users to define:
expires_at
When the URL expires:
GET /aB7xK2
↓
Expired
↓
410 Gone
An expiration feature introduces practical database and caching concerns.
You must consider:
- timezone handling
- cache expiration
- cleanup
- race conditions
- user expectations
Step 13 — Write tests
Do not wait until the end.
Start testing early.
Important test cases include:
Create valid URL
Create invalid URL
Duplicate short code
Unknown short code
Expired URL
Disabled URL
Authenticated request
Unauthorized request
Redirect behavior
Analytics recording
Rate limiting
Organize tests around behavior rather than implementation details.
For example:
test_expired_url_returns_gone()
is more useful than a test that only verifies an internal function was called.
Step 14 — Add integration tests
Unit tests are not enough.
Test the complete flow:
POST URL
↓
Database
↓
GET short URL
↓
Redirect
↓
Analytics
This verifies that your components actually work together.
Step 15 — Containerize with Docker
Once the application works locally, containerize it.
A production-style environment may include:
FastAPI
PostgreSQL
Redis
Docker lets you define a reproducible development environment.
A simple local setup might use:
docker compose up
with services for:
api
db
redis
Keep environment-specific secrets outside the image.
Step 16 — Add configuration management
Do not hard-code:
DATABASE_URL
SECRET_KEY
REDIS_URL
Use environment variables.
For example:
DATABASE_URL=...
REDIS_URL=...
SECRET_KEY=...
Keep secrets out of Git.
Your repository should include an example configuration file such as:
.env.example
without real credentials.
Step 17 — Add CI/CD
Configure GitHub Actions to run:
Push
↓
Install dependencies
↓
Lint
↓
Tests
↓
Build
↓
Deploy
A pull request should not be merged if the test suite fails.
CI is not just a deployment tool.
It creates a repeatable quality gate.
Step 18 — Add logging
A production service needs useful logs.
Examples:
request_id
endpoint
status_code
latency
user_id
error_type
Avoid logging secrets or unnecessary personal information.
Structured logs make debugging and analysis easier.
Step 19 — Add monitoring
Monitor the things that matter.
For example:
Requests / second
Error rate
Redirect latency
Database latency
Cache hit rate
API latency
A useful metric for the caching layer is:
Cache Hit Rate
=
Cache Hits / Total Cache Lookups
If your cache hit rate is poor, the cache may not be helping much.
Measure before optimizing.
Step 20 — Deploy
Deploy the service to a cloud platform that supports your stack.
The deployment architecture could be:
Internet
↓
HTTPS
↓
Application
↓
PostgreSQL
↓
Redis
Configure:
- environment variables
- database connection
- health checks
- HTTPS
- logging
- monitoring
Start with one service instance.
You do not need Kubernetes for the first deployment.
Health Checks
Add an endpoint such as:
GET /health
A healthy response might be:
{
"status": "ok"
}
For a more serious system, the health check can distinguish application health from dependency health.
For example:
Application: healthy
Database: healthy
Redis: degraded
Do not make a simple health endpoint perform expensive work.
System Design Questions
Once the basic application works, start thinking like a backend engineer.
What happens if two requests generate the same short code?
The database must enforce uniqueness.
What happens if Redis is unavailable?
The system should usually fall back to PostgreSQL rather than becoming completely unavailable.
What happens if PostgreSQL is slow?
Requests may become slow.
You need timeouts and monitoring.
What happens when one URL receives millions of visits?
Caching and horizontal scaling may become important.
What happens when a link is deleted?
Invalidate or update related cached state.
What happens if the service restarts?
Persistent data must live outside the application process.
These questions are where a small project turns into system-design practice.
What you will learn
By completing this project, you should understand:
- REST API design
- HTTP methods and status codes
- request validation
- service-layer architecture
- PostgreSQL schema design
- database indexes
- uniqueness constraints
- transactions
- Redis caching
- cache invalidation
- authentication
- rate limiting
- testing
- Docker
- CI/CD
- logging
- monitoring
- deployment
- basic scalability
Version roadmap
Version 1 — Basic API
FastAPI
↓
PostgreSQL
↓
Create + Redirect URLs
Goal: understand HTTP and database-backed APIs.
Version 2 — User Accounts
Add:
- authentication
- user-owned links
- management endpoints
- expiration
Version 3 — Analytics
Add:
- clicks
- timestamps
- basic referrer tracking
- dashboard
Version 4 — Performance
Add:
- Redis
- cache hit metrics
- rate limiting
Version 5 — Production
Add:
- Docker
- tests
- CI/CD
- health checks
- structured logging
- monitoring
Do not start at Version 5.
Build each layer deliberately.
Possible improvements
After the core system works, you can add:
- custom aliases
- QR code generation
- link previews
- password-protected links
- scheduled expiration
- team accounts
- API keys
- usage limits
- custom domains
- analytics dashboards
- bulk URL creation
Pick improvements that reinforce the skills you want to learn.
GitHub repository structure
A practical repository could look like:
url-shortener/
│
├── app/
│ ├── api/
│ ├── core/
│ ├── models/
│ ├── repositories/
│ ├── services/
│ └── main.py
│
├── tests/
│ ├── unit/
│ └── integration/
│
├── migrations/
├── scripts/
│
├── Dockerfile
├── compose.yaml
├── .env.example
├── README.md
└── pyproject.toml
The exact structure can change.
The important principle is separation of concerns.
What makes this project portfolio-worthy?
A basic URL shortener is a common project.
The value comes from the engineering depth you add.
Instead of:
“Built a URL shortener with FastAPI.”
Aim for:
“Built a containerized URL-shortening service with PostgreSQL persistence, Redis caching, authenticated link management, rate limiting, integration tests, click analytics, CI/CD, and production monitoring.”
Then prove those claims in the repository.
Include:
- architecture diagram
- API documentation
- tests
- load-test results
- cache hit rate
- latency measurements
- deployment link
- failure analysis
Benchmark the system
Don’t just say it is fast.
Measure it.
For example:
Without cache:
Average redirect latency = X ms
With Redis:
Average redirect latency = Y ms
Cache hit rate:
Z%
The actual values depend on your environment.
The important part is that you measure before and after the optimization.
Common mistakes
Building too much too early
Start with the database-backed API.
Adding Redis without measuring
A cache is not automatically useful.
Hard-coding configuration
Use environment variables.
No tests
Even small services need tests.
Poor database design
Indexes and constraints matter.
Ignoring failure cases
Ask what happens when dependencies fail.
Using Kubernetes unnecessarily
Complex infrastructure is not a badge of engineering maturity.
Writing one giant file
Separate API, services, repositories, and configuration as the project grows.
Final challenge
Once the basic URL shortener works, run a small load test.
Then ask:
- What is the average latency?
- What is the p95 latency?
- How many requests can the service handle?
- What percentage of requests hit Redis?
- What happens when Redis is unavailable?
- What happens when the database is unavailable?
- Where is the bottleneck?
Then improve one bottleneck at a time.
That exercise will teach you more about backend engineering than adding another five endpoints.
What you’ll learn
This project is a foundation for larger software systems.
It prepares you for:
REST APIs
↓
Database Applications
↓
Caching
↓
Authentication
↓
Distributed Systems
↓
Cloud Deployment
↓
System Design
It also transfers directly to AI engineering.
The same concepts appear in:
- model-serving APIs
- RAG applications
- AI agents
- inference services
- analytics systems
- AI SaaS products
Final thought
The best software projects don’t become valuable because they are large.
They become valuable because they force you to understand how real systems behave.
This project starts with a simple idea:
Long URL
↓
Short URL
and gradually introduces the engineering realities behind a reliable service.
That is exactly the kind of progression a strong software portfolio should show.