ShortLink is a backend REST API that shortens long URLs and tracks click analytics. The core focus of this project is caching and performance using Redis:
- Redirects use a cache-aside pattern: the short code to URL mapping is checked in Redis first, falling back to PostgreSQL only on a cache miss, then populating the cache for subsequent requests.
- Rate limiting is implemented with Redis using a fixed-window counter, protecting both the redirect endpoint and link creation from abuse.
- Click tracking is recorded asynchronously so it never slows down the redirect response.
This project is built as a backend portfolio piece to demonstrate:
- β Cache-aside pattern with Redis (cache hit/miss handling)
- β Redis-based rate limiting (fixed-window counter with TTL)
- β JWT authentication
- β Click analytics with async recording
- β Dockerized setup (app + PostgreSQL + Redis in one command)
flowchart LR
Client[Client / Postman] -->|GET /:code| API[Gin REST API]
API -->|1. Check cache| Redis[(Redis)]
Redis -->|Cache hit| API
API -.->|Cache miss| DB[(PostgreSQL)]
DB -.->|2. Fetch URL| API
API -.->|3. Populate cache| Redis
API -->|302 Redirect| Client
API -->|async| Visits[Record visit + increment counter]
Visits --> DB
| Component | Technology |
|---|---|
| Language | Go 1.22 |
| Web Framework | Gin |
| Database | PostgreSQL |
| Cache / Rate Limiter | Redis |
| Auth | JWT (golang-jwt) |
| Password Hashing | bcrypt |
| Containerization | Docker & Docker Compose |
shortlink/
βββ main.go # entry point
βββ config/database.go # PostgreSQL + Redis connection, auto-migration
βββ models/models.go # structs & request payloads
βββ middleware/auth.go # JWT auth & Redis-based rate limiter
βββ handlers/
β βββ auth_handler.go # register & login
β βββ link_handler.go # create/delete link, redirect (cache-aside), stats, QR code, my-links
βββ validators/validators.go # custom validators (URL scheme, link code format)
βββ routes/routes.go # route definitions
βββ postman/ # Postman collection for testing
βββ Dockerfile
βββ docker-compose.yml
βββ README.md
Note on database schema: tables are created automatically at startup via
RunMigrations()inconfig/database.go(usingCREATE TABLE IF NOT EXISTS), so no manual setup step is needed.
This spins up the API, PostgreSQL, and Redis with a single command.
git clone https://github.com/0xrayn/shortlink.git
cd shortlink
docker-compose up --buildThe API will be available at http://localhost:8080.
Requires Go 1.22+, a running PostgreSQL instance, and a running Redis instance.
git clone https://github.com/0xrayn/shortlink.git
cd shortlink
cp .env.example .env
# Edit .env to match your local PostgreSQL and Redis configuration
go mod tidy
go run main.go| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /auth/register |
Register a new user | - |
| POST | /auth/login |
Login and get JWT token | - |
Register
POST /auth/register
{
"email": "user@example.com",
"password": "secret123"
}Login
POST /auth/login
{
"email": "user@example.com",
"password": "secret123"
}| Method | Endpoint | Description | Auth | Rate Limit |
|---|---|---|---|---|
| POST | /shorten |
Create a short link | Required | 10 req/min per IP |
| GET | /:code |
Redirect to the original URL | - | 60 req/min per IP |
| GET | /:code/stats |
Get click stats for a link | - | - |
| GET | /:code/qr |
Get a QR code (PNG) pointing to the short link | - | - |
| DELETE | /:code |
Delete a link (owner only) | Required | - |
| GET | /my-links |
List links created by the logged-in user | Required | - |
Create Short Link
POST /shorten
Authorization: Bearer <token>
{
"url": "https://github.com/0xrayn/shortlink-api"
}Optionally provide a custom alias:
{
"url": "https://github.com/0xrayn/shortlink-api",
"code": "my-repo"
}Response:
{
"id": 1,
"code": "aB3xY9",
"short_url": "http://localhost:8080/aB3xY9",
"original_url": "https://github.com/0xrayn/shortlink-api",
"created_at": "2026-06-14T10:00:00Z"
}URL validation: the url field must be a valid http:// or https:// URL (other schemes such as javascript:, ftp:, or file: are rejected). The optional code field, if provided, must be 3-30 characters and contain only letters, numbers, hyphens, and underscores.
Redirect
GET /aB3xY9
Returns 302 Found with Location header set to the original URL. The first request is a cache miss (reads from PostgreSQL and populates Redis); subsequent requests are served from Redis until the cache entry expires (1 hour TTL).
Get Link Stats
GET /aB3xY9/stats
{
"code": "aB3xY9",
"original_url": "https://github.com/0xrayn/shortlink-api",
"click_count": 5,
"created_at": "2026-06-14T10:00:00Z",
"recent_visits": [
{ "visited_at": "2026-06-14T10:05:00Z", "ip_address": "127.0.0.1", "user_agent": "PostmanRuntime/7.36.0" }
]
}Get QR Code
GET /aB3xY9/qr
Returns a 256x256 PNG image encoding the short URL (http://localhost:8080/aB3xY9). Open this URL directly in a browser to see the QR code, or scan it with a phone camera to be redirected to the original link.
Delete Link
DELETE /aB3xY9
Authorization: Bearer <token>
Deletes the link and its click history. Only the user who created the link can delete it; attempting to delete someone else's link (or a non-existent code) returns 404.
Get My Links
GET /my-links?page=1&limit=10
Authorization: Bearer <token>
{
"data": [ ... ],
"pagination": { "page": 1, "limit": 10, "total": 3 }
}| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
The most important part of this codebase is RedirectLink in handlers/link_handler.go. Every redirect follows the cache-aside pattern:
- Check Redis for
link:<code>. - Cache hit -> parse the composite value (stored as
link_id|original_url). Use theoriginal_urlfor the redirection and thelink_idto record the visit. This completely eliminates database queries during cache hits. - Cache miss -> query PostgreSQL for both
idandoriginal_url, then store it in Redis aslink_id|original_urlwith a TTL (1 hour) for future requests. - Record the visit (insert into
link_visits, incrementclick_count) asynchronously in a goroutine, so the redirect response is never delayed by write operations.
Note on Backwards Compatibility: The cache parser is fully backwards-compatible. If it encounters a legacy cache entry containing only the URL, it automatically queries the database once to retrieve the link ID and updates the cache to the new composite format (
link_id|original_url) on the fly.
middleware.RateLimit(limit, window) implements a fixed-window counter using Redis:
- Each
(route, IP)pair gets a Redis key with an incrementing counter and a TTL equal to the window duration. - If the counter exceeds the limit before the TTL expires, the request is rejected with
429 Too Many Requestsand aretry_afterfield (seconds until the window resets). /shortenis limited to 10 requests/minute per IP (prevent spam link creation)./:code(redirect) is limited to 60 requests/minute per IP (prevent abuse while allowing normal traffic).
A ready-to-use Postman collection is included at postman/ShortLink_API.postman_collection.json, covering:
- Register & login (with automatic token extraction)
- Create short link (random code and custom alias)
- Redirect (cache miss -> cache hit flow)
- Click stats verification
- Pagination on
/my-links - 404 for non-existent codes
- Rate limit behavior (
429after exceeding the limit)
How to run:
- Open Postman -> Import -> select
postman/ShortLink_API.postman_collection.json - Make sure the API is running (
docker-compose uporgo run main.go) - Click the collection -> Run -> Run ShortLink API
All requests include pm.test() assertions, so the runner gives a pass/fail summary automatically - no manual checking needed.
Screenshots of the API tested via Postman:
Create short link
Redirect with cache
Link stats
Rate limit response
QR code (open directly in browser)
- JWT authentication
- Cache-aside pattern with Redis
- Redis-based rate limiting
- Click analytics with async recording
- Pagination
- Dockerized setup
- Postman collection with automated tests
- Deploy to Railway/Render




