ScaleURL is a backend-only URL shortener built with Go, PostgreSQL, and Redis. It started as a portfolio MVP and has now been hardened into a more deployable, testable, and operationally safer service.
There is currently no frontend in this repository. The product surface is an HTTP API plus the public redirect endpoint.
Version 1 established the core product flow:
- JWT-based auth for register/login
- authenticated short-link creation
- public redirect endpoint
- PostgreSQL persistence for users, links, and clicks
- Redis cache-aside redirect flow
- Redis-backed rate limiting
- expiry support
- basic analytics
- Docker-based local setup
This was already a good backend prototype, but it still leaned heavily on manual testing and had only light operational hardening.
Version 2 keeps the same feature set and improves the service around reliability and deployability:
- stricter JSON parsing with unknown-field rejection
- stronger request validation for email, password, URL, and expiry input
/healthand/readyendpoints- graceful shutdown with connection cleanup
- HTTP server timeouts
- request ID and real-IP middleware
- automated unit tests for core helpers and handlers
- GitHub Actions CI for format, vet, test, and Docker build checks
cmd/server/main.go: app bootstrap, middleware stack, routes, server timeouts, graceful shutdowninternal/handlers: HTTP handlers plus validation and system endpointsinternal/auth: bcrypt password hashing and JWT creation/validationinternal/middleware: auth middleware and Redis-backed rate limitinginternal/db: shared PostgreSQL pool and startup migration runnerinternal/cache: shared Redis client, cached link helpers, rate-limit helpersinternal/models: API/data structsmigrations: raw SQL schema files
- Client sends
POST /api/shortenwith a JWT. - Request body is size-limited and validated.
- A Base62 code is generated.
- The link is inserted into PostgreSQL.
- Redis is pre-warmed with link metadata.
- Client opens
GET /:code. - Redis is checked first.
- Active/expiry rules are enforced on cached and DB-loaded links.
- The browser receives
302 Found. - Click logging runs asynchronously and updates analytics counters.
- Owner calls
GET /api/analytics/:id. - Link ownership is verified.
- PostgreSQL aggregates totals, last-24h usage, device split, and top referrers.
- Go + Chi
- PostgreSQL +
pgx - Redis +
go-redis - JWT auth
- Docker / Docker Compose
- GitHub Actions
docker compose up -dIf you do not have Redis running locally, uncomment the Redis service in docker-compose.yml and start Compose again.
The project uses .env:
PORT=8080
BASE_URL=http://localhost:8080
DB_URL=postgres://scaleurl:secret@localhost:5432/scaleurl?sslmode=disable
REDIS_URL=redis://localhost:6379
JWT_SECRET=change-this-to-a-long-random-secret-in-production
JWT_EXPIRY_HOURS=72go run ./cmd/serverThe app loads env vars, connects to PostgreSQL and Redis, runs migrations, and starts the HTTP server.
| Method | Route | Auth | Purpose |
|---|---|---|---|
GET |
/health |
No | Liveness check |
GET |
/ready |
No | Readiness check for DB + Redis |
POST |
/auth/register |
No | Create account and return JWT |
POST |
/auth/login |
No | Login and return JWT |
POST |
/api/shorten |
Yes | Create short link |
GET |
/api/links |
Yes | List active links for current user |
DELETE |
/api/links/{id} |
Yes | Soft-delete a link |
GET |
/api/analytics/{id} |
Yes | View analytics for one link |
GET |
/{code} |
No | Redirect to original URL |
- register a user and receive
201 - login and receive
200 - create a link and receive
201 - open
/{code}and receive302 - open analytics for the returned link
idand see click data - delete the link and receive
204 - open
/{code}again and receive404
- register with malformed email and expect
400 - register with a short password and expect
400 - send unknown JSON fields and expect
400 - call protected routes without
Authorization: Bearer <token>and expect401 - create a link with an invalid or non-HTTP URL and expect
400 - create a link with negative
expires_in_daysand expect400
- create a link with expiry, set
expires_atin the DB to the past, and expect410 Goneon redirect - stop Redis or Postgres and expect
/readyto return503 - keep the server process alive and expect
/healthto still return200
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"demo@example.com","password":"secret123"}'curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"demo@example.com","password":"secret123"}'curl -X POST http://localhost:8080/api/shorten \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/docs","expires_in_days":30}'curl http://localhost:8080/api/links \
-H "Authorization: Bearer YOUR_JWT"curl http://localhost:8080/api/analytics/LINK_ID \
-H "Authorization: Bearer YOUR_JWT"curl -X DELETE http://localhost:8080/api/links/LINK_ID \
-H "Authorization: Bearer YOUR_JWT"Run the local verification suite with:
go test ./...CI is defined in .github/workflows/ci.yml and runs:
gofmtgo vetgo test ./...docker build
Dockerfileis already multi-stage and suitable for platforms like Railway or Render.- Production hosting should inject env vars directly instead of relying on
.env. - The app now has readiness and shutdown behavior that make platform deployments safer than the original MVP.
Today this project is more than just a proof of concept:
- Version 1 proved the product and architecture.
- Version 2 makes it easier to deploy, verify, and operate.
It is still best described as a production-style MVP backend rather than a fully enterprise-hardened system, but it is meaningfully stronger now than the original prototype.
For project structure and architecture flow, see ARCHITECTURE.md.