Skip to content

API

This document provides information about the mjr.wtf URL shortener API.

The complete API is documented using OpenAPI 3.0. The specification file is located at openapi.yaml in the repository root.

Interactive Documentation:

  • SwaggerUI - Interactive API documentation with “Try it out” feature
  • ReDoc - Clean, responsive API reference documentation

Local Validation:

Terminal window
# Using Make
make validate-openapi
# Using swagger-cli directly
npm install -g @apidevtools/swagger-cli
swagger-cli validate openapi.yaml
  • Production: https://mjr.wtf
  • Local Development: http://localhost:8080

Most endpoints require Bearer token authentication:

Terminal window
Authorization: Bearer YOUR_TOKEN_HERE

Configure your token via AUTH_TOKENS (preferred, comma-separated) or AUTH_TOKEN (legacy). See the Authentication section in the main README for details.

All API requests and responses use application/json content type unless otherwise specified.

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:

Terminal window
curl -X POST https://mjr.wtf/api/urls \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"original_url": "https://example.com"}'

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:

Terminal window
curl https://mjr.wtf/api/urls?limit=10&offset=0 \
-H "Authorization: Bearer YOUR_TOKEN"

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:

Terminal window
curl -X DELETE https://mjr.wtf/api/urls/abc123 \
-H "Authorization: Bearer YOUR_TOKEN"

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:

Terminal window
curl https://mjr.wtf/api/urls/abc123/analytics \
-H "Authorization: Bearer YOUR_TOKEN"

Example - Time range:

Terminal window
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"

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:

Terminal window
curl -L https://mjr.wtf/abc123

GET /health

Lightweight liveness check. Does not validate external dependencies.

GET /ready

Readiness check that validates dependencies (currently: database connectivity).

Authentication: None

Response (200 OK):

{
"status": "ok"
}

Example:

Terminal window
curl https://mjr.wtf/health

Readiness Response (200 OK):

{
"status": "ready"
}

Readiness Response (503 Service Unavailable):

{
"status": "unavailable"
}

Readiness Example:

Terminal window
curl https://mjr.wtf/ready

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:

Terminal window
# If authentication is enabled
curl https://mjr.wtf/metrics \
-H "Authorization: Bearer YOUR_TOKEN"
# If authentication is disabled (default)
curl https://mjr.wtf/metrics

All error responses follow a consistent format:

{
"error": "error message here"
}
  • 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

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)"
}
{
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
}
{
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)
}
  • Length: 3-20 characters
  • Allowed characters: alphanumeric, underscore, hyphen (a-zA-Z0-9_-)
  • Pattern: ^[a-zA-Z0-9_-]{3,20}$
  • Must include scheme (http:// or https://)
  • Must have a valid host
  • Must be a valid URL format
  • Both start_time and end_time must be provided together
  • Times must be in RFC3339 format (e.g., 2025-11-20T00:00:00Z)
  • start_time must be strictly before end_time

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).

The OpenAPI specification is automatically validated in CI:

.github/workflows/ci.yml
- name: Validate OpenAPI spec
run: |
npm install -g @apidevtools/swagger-cli
swagger-cli validate openapi.yaml

Before committing changes to the OpenAPI spec:

Terminal window
# Validate the spec
make validate-openapi
# Run all checks (includes OpenAPI validation)
make check

When making API changes:

  1. Update the code - Make changes to handlers, request/response types
  2. Update the OpenAPI spec - Update openapi.yaml to reflect the changes
  3. Validate the spec - Run make validate-openapi to ensure it’s valid
  4. Update tests - Ensure integration tests cover the new/changed behavior
  5. Update examples - Update code examples in this document if needed
  6. Commit together - Commit code changes and spec updates together

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.

The OpenAPI spec can be used to generate client libraries in various languages:

Terminal window
# Install OpenAPI Generator
npm install -g @openapitools/openapi-generator-cli
# Generate a TypeScript client
openapi-generator-cli generate -i openapi.yaml -g typescript-fetch -o clients/typescript
# Generate a Python client
openapi-generator-cli generate -i openapi.yaml -g python -o clients/python
# Generate a Go client
openapi-generator-cli generate -i openapi.yaml -g go -o clients/go

Use the OpenAPI spec for automated API testing:

Terminal window
# Install Dredd for API testing
npm install -g dredd
# Test API against the spec
dredd openapi.yaml http://localhost:8080

Create a mock server from the spec:

Terminal window
# Install Prism
npm install -g @stoplight/prism-cli
# Run mock server
prism mock openapi.yaml

For issues or questions about the API: