Ctrl K
ComunidadGuías…

README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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