A simple REST URL shortening service built with Go & MySQL. This service allows users to create short URLs, which then can be deleted by Admins using an API Key.
- Shorten long URLs with custom expiration times
- Delete URLs by ID with API key authentication
- RESTful API with JSON responses
- MySQL database with migrations
- Docker containerisation
- Base62 encoding for short codes
- URL expiration validation
- Comprehensive test coverage with mocks
The project follows clean architecture principles with clear separation of concerns:
internal/
├── app/ # Application layer coordination
├── config/ # Configuration management
├── domain/ # Core business entities (URL)
├── service/ # Business logic layer
├── store/ # Data persistence layer
├── mocks/ # Generated mock implementations for testing
└── transport/ # Transport layers
└── httpapi/ # HTTP transport layer with handlers, routing, and middleware
- Docker
- Go 1.21+ (for development)
- Make (for using Makefile commands)
- bash
Create a .env file in the project root (or copy the env-sample to .env) with the following values:
# Database Configuration
MYSQL_ROOT_PASSWORD=password
MYSQL_DATABASE=url_shortener
MYSQL_USER=url_shortener_user
MYSQL_PASSWORD=url_shortener_pass
DB_USER=url_shortener_user
MYSQL_HOST=localhost
API_KEY='sample_secure_api_key' # API key for authenticating delete requests
#
DSN='url_shortener_user:url_shortener_pass@tcp(localhost:3306)/url_shortener?parseTime=true'
# Port Configuration
MYSQL_PORT=3306
APP_PORT=12345
BASE_URL=http://localhost:12345make db-up # Start MySQL container
make migrate-up # Run database migrations
make app-up # Start application locally- POST /v1/urls - Create short URL (no auth needed)
- GET /{shortcode} - Redirect to original URL (no auth needed)
- GET /v1/urls/{uuid} - Get URL metadata for admin/management
- DELETE /v1/urls/{uuid} - Delete URL by UUID
- Create a new short URL:
curl -d '{ "long_url": "https://www.example3.com", "expires_after": "24h" }' http://localhost:12345/v1/urls
{"short_code":"qBdSY4","id":"0198f68d-a873-742d-831f-cd3d61e78bda"}- You should now be redirected to
https://www.example3.comwhen youcurlthe following:
curl -v http://localhost:12345/qBdSY4
...
...
...
< HTTP/1.1 302 Found
< Content-Type: text/html; charset=utf-8
< Location: https://www.example3.com
< Vary: Origin
< Date: Fri, 29 Aug 2025 16:00:44 GMT
< Content-Length: 47
<
<a href="https://www.example3.com">Found</a>.
- Using the API Keys you set up in the .env file, you should be able to get the URL details:
curl -H "X-API-Key: sample_secure_api_key" http://localhost:12345/v1/urls/0198f68d-a873-742d-831f-cd3d61e78bda
{"id":"0198f68d-a873-742d-831f-cd3d61e78bda","code":"qBdSY4","long_url":"https://www.example3.com","created_at":"2025-08-29T15:58:56Z","expires_at":"2025-08-30T15:58:56Z"}- Using the API Key you set up in the .env file, you can delete the record from the DB (note the UUID from the previous response):
# This will succeed with correct API key from .env file
curl -X DELETE -H "X-API-Key: sample_secure_api_key" http://localhost:12345/v1/urls/0198f68d-a873-742d-831f-cd3d61e78bda
{}
http://localhost:12345
POST /v1/urls
Content-Type: application/json
{
"long_url": "https://example.com/very-long-url-that-needs-shortening",
"expires_after": "168h" // Duration string (e.g., "1h", "30m", "24h", "7d")
}Response:
{
"short_code": "AbC123",
"id": "550e8400-e29b-41d4-a716-446655440000"
}GET /{short_code}Response:
HTTP 302 Found
Location: https://example.com/original-long-url
Error Responses:
400 Bad Request: Invalid short code format404 Not Found: Short code not found410 Gone: URL has expired
All protected endpoints require the X-API-Key header with a valid API key.
GET /v1/urls/{uuid}
X-API-Key: your-secret-api-keyResponse:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "AbC123",
"long_url": "https://example.com/original-long-url",
"created_at": "2025-08-29T15:58:56Z",
"expires_at": "2025-08-30T15:58:56Z"
}DELETE /v1/urls/{uuid}
X-API-Key: your-secret-api-keyResponse:
{
"ok": "true"
}Error Responses for Protected Endpoints:
400 Bad Request: Invalid UUID format401 Unauthorized: Missing or invalid API key404 Not Found: URL not found500 Internal Server Error: Server error
Supported Duration Formats:
- Hours:
1h,24h,168h - Minutes:
30m,90m - Days:
1d,7d(converted to hours) - Combinations:
1h30m,2h45m
URL Validation:
- Must include valid HTTP/HTTPS scheme
- Maximum request body size: 256KB
- Short codes are exactly 6 characters (Base62 encoded)
- UUIDs follow standard UUID v4 format
Authentication:
- API key must be provided in
X-API-Keyheader - Only admin/management endpoints require authentication
- Public creation and redirect endpoints are open
The application uses environment variables for configuration:
| Variable | Description | Example | Required |
|---|---|---|---|
BASE_URL |
Base URL for shortened URLs | http://localhost:12345 |
Yes |
APP_PORT |
Application port | 12345 |
Yes |
API_KEY |
API key for DELETE operations | your-secret-key |
Yes |
MYSQL_* |
MySQL configuration for Docker | See env example | Yes |
├── cmd/server/ # Application entry point
├── internal/
│ ├── app/ # Application coordination layer
│ ├── config/ # Configuration management
│ ├── domain/ # Core entities (URL)
│ ├── service/ # Business logic (URL service)
│ ├── store/ # Data access layer
│ ├── mocks/ # Generated test mocks
│ ├── transport/httpapi/ # HTTP handlers, middleware, routing
│ └── db-migrations/mysql/ # Database migrations
├── Makefile # Build and development commands
├── go.mod # Go modules
└── README.md # This file
# Run all tests
go test ./...
# Run tests for specific package
go test ./internal/transport/httpapi
go test ./internal/service
# Run tests with coverage
go test -cover ./...
# Run specific test case
go test -run TestCreateShortURLHandler ./internal/transport/httpapi# Generate mocks from interfaces
make gen-mocks
# This generates mocks for:
# - internal/store/store.go -> internal/mocks/store_mock.go
# - internal/service/url_service.go -> internal/mocks/url_service_mock.go- DELETE operations require API key authentication
- API key must be provided in
X-API-Keyheader - Only DELETE requests are protected (create operations are public)
- URLs must have valid HTTP/HTTPS schemes
- UUID validation for resource IDs
- Request body size limited to 256KB
- Duration strings validated using Go's
time.ParseDuration
This project is licensed under the MIT License.
- Introduce custom logging using
log/slog - Update error handling by polishing
transport/httpapi/errors.go - Add E2E testing using test containers