API
This document provides information about the mjr.wtf URL shortener API.
OpenAPI Specification
Section titled “OpenAPI Specification”The complete API is documented using OpenAPI 3.0. The specification file is located at openapi.yaml in the repository root.
Viewing the Documentation
Section titled “Viewing the Documentation”Interactive Documentation:
- SwaggerUI - Interactive API documentation with “Try it out” feature
- ReDoc - Clean, responsive API reference documentation
Local Validation:
# Using Makemake validate-openapi
# Using swagger-cli directlynpm install -g @apidevtools/swagger-cliswagger-cli validate openapi.yamlAPI Overview
Section titled “API Overview”Base URLs
Section titled “Base URLs”- Production:
https://mjr.wtf - Local Development:
http://localhost:8080
Authentication
Section titled “Authentication”Most endpoints require Bearer token authentication:
Authorization: Bearer YOUR_TOKEN_HEREConfigure your token via AUTH_TOKENS (preferred, comma-separated) or AUTH_TOKEN (legacy). See the Authentication section in the main README for details.
Content Type
Section titled “Content Type”All API requests and responses use application/json content type unless otherwise specified.
Endpoints
Section titled “Endpoints”URL Management
Section titled “URL Management”Create Shortened URL
Section titled “Create Shortened URL”POST /api/urls
Creates a new shortened URL.
Authentication: Required
Request Body:
{ "original_url": "https://example.com/very/long/url/path"}Response (201 Created):
{ "short_code": "abc123", "short_url": "https://mjr.wtf/abc123", "original_url": "https://example.com/very/long/url/path"}Example:
curl -X POST https://mjr.wtf/api/urls \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"original_url": "https://example.com"}'List URLs
Section titled “List URLs”GET /api/urls
Retrieves a paginated list of URLs for the current auth identity.
Note: mjr.wtf currently maps all valid tokens to a single shared identity (created_by: "authenticated-user"), so this behaves like a single-tenant list.
Authentication: Required
Query Parameters:
limit(optional): Maximum number of URLs to return (0-100; values <= 0 use the default: 20)offset(optional): Number of URLs to skip for pagination (default: 0)
Response (200 OK):
{ "urls": [ { "id": 1, "short_code": "abc123", "original_url": "https://example.com", "created_at": "2025-12-25T10:00:00Z", "created_by": "authenticated-user", "click_count": 42 } ], "total": 1, "limit": 20, "offset": 0}Example:
curl https://mjr.wtf/api/urls?limit=10&offset=0 \ -H "Authorization: Bearer YOUR_TOKEN"Delete URL
Section titled “Delete URL”DELETE /api/urls/{shortCode}
Deletes a shortened URL. Requires authentication; only the creator (created_by) can delete the URL (returns 403 otherwise).
Authentication: Required
Path Parameters:
shortCode: The short code to delete (e.g., “abc123”)
Response (204 No Content): No response body.
Errors: 401 (unauthorized), 403 (forbidden), 404 (not found), 429 (rate limited)
Example:
curl -X DELETE https://mjr.wtf/api/urls/abc123 \ -H "Authorization: Bearer YOUR_TOKEN"Analytics
Section titled “Analytics”Get URL Analytics
Section titled “Get URL Analytics”GET /api/urls/{shortCode}/analytics
Retrieves analytics data for a shortened URL including click counts, geographic distribution, and referrer information.
Authentication: Required; only the creator (created_by) can view analytics (returns 403 otherwise).
Note: mjr.wtf currently maps all valid tokens to a single shared identity (created_by: "authenticated-user"), so this typically behaves like a single-tenant deployment.
Path Parameters:
shortCode: The short code to get analytics for
Query Parameters:
start_time(optional): Filter clicks from this time (RFC3339 format, e.g., “2025-11-20T00:00:00Z”)end_time(optional): Filter clicks until this time (RFC3339 format, e.g., “2025-11-22T23:59:59Z”)
Note: Both start_time and end_time must be provided together for time range queries. start_time must be strictly before end_time.
Response (200 OK) - All-time statistics:
{ "short_code": "abc123", "original_url": "https://example.com", "total_clicks": 150, "by_country": { "US": 75, "GB": 30, "DE": 25 }, "by_referrer": { "https://twitter.com": 50, "direct": 60 }, "by_date": { "2025-12-20": 30, "2025-12-21": 45 }}Response (200 OK) - Time range statistics:
{ "short_code": "abc123", "original_url": "https://example.com", "total_clicks": 75, "by_country": { "US": 40, "GB": 20 }, "by_referrer": { "https://twitter.com": 30, "direct": 45 }, "start_time": "2025-11-20T00:00:00Z", "end_time": "2025-11-22T23:59:59Z"}Example - All-time:
curl https://mjr.wtf/api/urls/abc123/analytics \ -H "Authorization: Bearer YOUR_TOKEN"Example - Time range:
curl "https://mjr.wtf/api/urls/abc123/analytics?start_time=2025-11-20T00:00:00Z&end_time=2025-11-22T23:59:59Z" \ -H "Authorization: Bearer YOUR_TOKEN"Public Endpoints
Section titled “Public Endpoints”Redirect
Section titled “Redirect”GET /{shortCode}
Redirects to the original URL associated with the short code. This endpoint is public and does not require authentication.
Path Parameters:
shortCode: The short code to redirect (e.g., “abc123”)
Response (302 Found):
Redirects to the original URL via the Location header.
Response (404 Not Found): Returns HTML page if short code doesn’t exist.
Example:
curl -L https://mjr.wtf/abc123Health & Monitoring
Section titled “Health & Monitoring”Health Check (Liveness)
Section titled “Health Check (Liveness)”GET /health
Lightweight liveness check. Does not validate external dependencies.
Readiness Check
Section titled “Readiness Check”GET /ready
Readiness check that validates dependencies (currently: database connectivity).
Authentication: None
Response (200 OK):
{ "status": "ok"}Example:
curl https://mjr.wtf/healthReadiness Response (200 OK):
{ "status": "ready"}Readiness Response (503 Service Unavailable):
{ "status": "unavailable"}Readiness Example:
curl https://mjr.wtf/readyPrometheus Metrics
Section titled “Prometheus Metrics”GET /metrics
Returns Prometheus metrics for monitoring.
Authentication: Optional (configurable via METRICS_AUTH_ENABLED environment variable)
Response (200 OK):
Returns Prometheus-formatted metrics in text/plain format.
Example:
# If authentication is enabledcurl https://mjr.wtf/metrics \ -H "Authorization: Bearer YOUR_TOKEN"
# If authentication is disabled (default)curl https://mjr.wtf/metricsError Responses
Section titled “Error Responses”All error responses follow a consistent format:
{ "error": "error message here"}HTTP Status Codes
Section titled “HTTP Status Codes”- 200 OK - Request succeeded
- 201 Created - Resource successfully created
- 204 No Content - Request succeeded with no response body
- 302 Found - Redirect response
- 400 Bad Request - Invalid input or request format
- 401 Unauthorized - Missing or invalid authentication token
- 403 Forbidden - Insufficient permissions (e.g., trying to delete another user’s URL)
- 404 Not Found - Resource not found
- 409 Conflict - Resource already exists (e.g., duplicate short code)
- 429 Too Many Requests - Rate limit exceeded
- 500 Internal Server Error - Server error
Common Error Examples
Section titled “Common Error Examples”Missing authentication:
{ "error": "Unauthorized: missing authorization header"}Invalid URL format:
{ "error": "original URL must be a valid http or https URL"}URL not found:
{ "error": "URL not found"}Unauthorized deletion:
{ "error": "unauthorized to delete this URL"}Invalid time range:
{ "error": "start_time must be strictly before end_time (equality not allowed)"}Data Models
Section titled “Data Models”URL Response Object
Section titled “URL Response Object”{ id: number; // Unique identifier short_code: string; // Short code (3-20 alphanumeric, underscore, hyphen) original_url: string; // Original URL created_at: string; // ISO 8601 timestamp created_by: string; // User ID of creator click_count: number; // Total number of clicks}Analytics Response Object
Section titled “Analytics Response Object”{ short_code: string; // Short code original_url: string; // Original URL total_clicks: number; // Total click count by_country: { [country: string]: number }; // Clicks by country (ISO 3166-1 alpha-2) by_referrer: { [referrer: string]: number }; // Clicks by referrer URL by_date?: { [date: string]: number }; // Clicks by date (YYYY-MM-DD) - only for all-time stats start_time?: string; // Start time (if time range query) end_time?: string; // End time (if time range query)}Validation Rules
Section titled “Validation Rules”Short Code
Section titled “Short Code”- Length: 3-20 characters
- Allowed characters: alphanumeric, underscore, hyphen (
a-zA-Z0-9_-) - Pattern:
^[a-zA-Z0-9_-]{3,20}$
Original URL
Section titled “Original URL”- Must include scheme (
http://orhttps://) - Must have a valid host
- Must be a valid URL format
Time Range Queries
Section titled “Time Range Queries”- Both
start_timeandend_timemust be provided together - Times must be in RFC3339 format (e.g.,
2025-11-20T00:00:00Z) start_timemust be strictly beforeend_time
Rate Limiting
Section titled “Rate Limiting”Rate limiting is implemented on the redirect endpoint and authenticated API routes. Configure via REDIRECT_RATE_LIMIT_PER_MINUTE (default: 120) and API_RATE_LIMIT_PER_MINUTE (default: 60).
Keeping the Spec in Sync
Section titled “Keeping the Spec in Sync”Automated Validation
Section titled “Automated Validation”The OpenAPI specification is automatically validated in CI:
- name: Validate OpenAPI spec run: | npm install -g @apidevtools/swagger-cli swagger-cli validate openapi.yamlManual Validation
Section titled “Manual Validation”Before committing changes to the OpenAPI spec:
# Validate the specmake validate-openapi
# Run all checks (includes OpenAPI validation)make checkProcess for Keeping Spec in Sync
Section titled “Process for Keeping Spec in Sync”When making API changes:
- Update the code - Make changes to handlers, request/response types
- Update the OpenAPI spec - Update
openapi.yamlto reflect the changes - Validate the spec - Run
make validate-openapito ensure it’s valid - Update tests - Ensure integration tests cover the new/changed behavior
- Update examples - Update code examples in this document if needed
- Commit together - Commit code changes and spec updates together
CI Enforcement
Section titled “CI Enforcement”The CI pipeline will fail if:
- The OpenAPI spec is invalid
- The spec doesn’t validate against the OpenAPI 3.0 schema
This ensures the specification stays accurate and up-to-date with the implementation.
Tools and Integrations
Section titled “Tools and Integrations”Code Generation
Section titled “Code Generation”The OpenAPI spec can be used to generate client libraries in various languages:
# Install OpenAPI Generatornpm install -g @openapitools/openapi-generator-cli
# Generate a TypeScript clientopenapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o clients/typescript
# Generate a Python clientopenapi-generator-cli generate -i openapi.yaml -g python -o clients/python
# Generate a Go clientopenapi-generator-cli generate -i openapi.yaml -g go -o clients/goAPI Testing
Section titled “API Testing”Use the OpenAPI spec for automated API testing:
# Install Dredd for API testingnpm install -g dredd
# Test API against the specdredd openapi.yaml http://localhost:8080Mock Server
Section titled “Mock Server”Create a mock server from the spec:
# Install Prismnpm install -g @stoplight/prism-cli
# Run mock serverprism mock openapi.yamlSupport
Section titled “Support”For issues or questions about the API:
- GitHub Issues: https://github.com/matt-riley/mjrwtf/issues
- OpenAPI Spec: https://github.com/matt-riley/mjrwtf/blob/main/openapi.yaml