# Linkas ![CI](https://github.com/galanjabal3/linkas/actions/workflows/ci.yml/badge.svg) ![License: MIT](https://img.shields.io/badge/license-MIT-7c3aed) A simple, fast URL shortener API built with Go + Gin + PostgreSQL. ## Features - **Shorten URLs** — Create short URLs with custom or auto-generated slugs - **Path-based Slugs** — Support nested paths like `video/01` - **QR Code** — Generate QR code images for short URLs - **Redirect** — Fast 301 redirects to original URLs - **Analytics** — Track clicks with IP, user-agent, and referrer - **Rate Limiting** — 30 requests per minute per IP - **URL Expiration** — Set TTL for short links with specific error handling - **Swagger Docs** — Interactive API documentation ## Tech Stack - Go 1.26+ - Gin (HTTP framework) - PostgreSQL (database) - sqlx (database toolkit) - go-qrcode (QR code generation) - Swaggo (Swagger docs) ## Quick Start ### 1. Clone & Setup ```bash git clone https://github.com/galanjabal3/linkas.git cd linkas cp .env.example .env ``` ### 2. Start PostgreSQL ```bash # Option A: Docker Compose (recommended) docker-compose up -d postgres # Option B: Manual Docker docker run -d --name linkas-db -p 5432:5432 -e POSTGRES_DB=linkas postgres:16-alpine ``` ### 3. Run Migrations ```bash go run cmd/migrate/main.go up ``` ### 4. Start API ```bash go run main.go ``` ### Full Stack with Docker ```bash docker-compose up -d ``` The Compose file starts PostgreSQL 16 (with `migrations/001_init.sql` mounted as `/docker-entrypoint-initdb.d` so the schema is created automatically) and builds the API from the multi-stage `Dockerfile` (Go 1.26-alpine builder → Alpine runtime, port 8080 exposed). ### Build & Run the Docker image manually ```bash docker build -t linkas . docker run -d --name linkas -p 8080:8080 \ -e PORT=8080 \ -e DATABASE_URL=postgres://postgres:postgres@localhost:5432/linkas?sslmode=disable \ -e BASE_URL=http://localhost:8080 \ linkas ``` The database must be reachable from the container — use the Compose stack (`DATABASE_URL=...@postgres:5432/...`) or point `DATABASE_URL` at a host PostgreSQL published on `localhost:5432`. ## API Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | POST | `/api/shorten` | Create short URL | | GET | `/:slug` | Redirect to original URL | | GET | `/api/stats/:slug` | Get click analytics | | GET | `/api/qrcode/:slug` | Generate QR code image | | DELETE | `/api/:slug` | Delete short URL | | GET | `/api/health` | Health check | Interactive Swagger UI: `http://localhost:8080/swagger/index.html` (spec served from `http://localhost:8080/doc.json`). ## API Usage Quick `curl` reference for every route (server defaults to `http://localhost:8080`): ```bash # Health check curl http://localhost:8080/api/health # → {"status":"healthy"} # Shorten a URL (returns 201) curl -X POST http://localhost:8080/api/shorten \ -H "Content-Type: application/json" \ -d '{"url":"https://github.com","expires_in":"24h"}' # Shorten with a custom slug — segments allowed, e.g. video/01 curl -X POST http://localhost:8080/api/shorten \ -H "Content-Type: application/json" \ -d '{"url":"https://github.com","custom_slug":"video/01"}' # Follow the short link (301 redirect; click is recorded) curl -i http://localhost:8080/video/01 # Click analytics curl http://localhost:8080/api/stats/video/01 # QR code PNG (256x256) curl -o qr.png http://localhost:8080/api/qrcode/video/01 # Delete a short URL curl -X DELETE http://localhost:8080/api/video/01 ``` > Rate limit: **30 requests/minute per IP** applies to all routes. ## Usage Examples ### Shorten a URL ```bash curl -X POST http://localhost:8080/api/shorten \ -H "Content-Type: application/json" \ -d '{"url": "https://github.com", "expires_in": "24h"}' ``` Response: ```json { "slug": "aB3xYz", "short_url": "http://localhost:8080/aB3xYz", "original_url": "https://github.com", "expires_at": "2026-08-06T10:00:00Z" } ``` ### Shorten with Custom Slug ```bash curl -X POST http://localhost:8080/api/shorten \ -H "Content-Type: application/json" \ -d '{"url": "https://github.com", "custom_slug": "my-link", "expires_in": "168h"}' ``` ### Get QR Code ```bash curl -o qr.png http://localhost:8080/api/qrcode/aB3xYz ``` Returns PNG image (256x256). ### Get Stats ```bash curl http://localhost:8080/api/stats/aB3xYz ``` Response: ```json { "slug": "aB3xYz", "original_url": "https://github.com", "click_count": 42, "created_at": "2026-08-05T10:00:00Z", "recent_clicks": [...] } ``` ### Delete URL ```bash curl -X DELETE http://localhost:8080/api/aB3xYz ``` ## Error Responses | Status | Error | Description | |--------|-------|-------------| | 400 | `invalid_request` | Malformed JSON body or field fails validation (`url` must be a valid URL) | | 400 | `shorten_failed` | URL has no host, or `custom_slug` fails validation (3–255 chars, 1–50 per segment) | | 404 | `not_found` | URL not found | | 409 | `shorten_failed` | Custom slug already taken | | 410 | `url_expired` | URL has expired | | 429 | `rate_limit_exceeded` | Too many requests (30/min per IP) | ## Project Structure ``` linkas/ ├── main.go # Entry point ├── cmd/ │ └── migrate/ # Migration runner ├── internal/ │ ├── handler/ # HTTP handlers │ ├── model/ # Data models │ ├── repository/ # Database queries │ ├── service/ # Business logic │ └── middleware/ # Rate limiting ├── migrations/ # SQL migrations ├── docker-compose.yml ├── Dockerfile └── .env.example ``` ## Migrations ```bash # Apply all migrations go run cmd/migrate/main.go up # Rollback last migration go run cmd/migrate/main.go down # Check current version go run cmd/migrate/main.go version ``` ## License MIT