diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..38000d7 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,33 @@ +name: Deploy API Documentation + +on: + push: + branches: + - master + paths: + - 'docs/openapi.yaml' + - '.github/workflows/deploy-docs.yml' + workflow_dispatch: + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate Swagger UI + uses: Legion2/swagger-ui-action@v1 + with: + output: swagger-ui + spec-file: docs/openapi.yaml + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: swagger-ui diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index 83a54fd..0000000 --- a/docs/API.md +++ /dev/null @@ -1,149 +0,0 @@ -# API Reference - -1. **Ping the API** - - - Endpoint: `GET /api/ping` - - Payload: None - - Response: 200 - - Response Example - ```json - { - "data": "pong" - } - ``` -2. **Redirect by Slug** - - - Endpoint: `GET /:slug` - - Payload: None - - Response: 301 - -3. **List All Links** - - - Endpoint: `GET /api/links` - - Headers: `X-Api-Key` - - Query Parameters: - - `limit` (optional): Number of results per page (default: 100) - - `cursor` (optional): Pagination cursor from previous response - - Response: 200 - - Response Example - ```json - { - "data": [ - { - "id": "84f0c7a4-8c4e-4665-b676-cb9c5e40f1db", - "refer": "http://localhost:4000/3wP4BQ", - "origin": "https://monocuco.donado.co" - } - ], - "pagination": { - "has_more": true, - "next": "75e0a7f4-9c5e-1235-b546-eb9c5e40f7ac" - } - } - ``` - -4. **List link by ID** - - Endpoint: `GET /api/links/:id` - - Headers: `X-Api-Key` - - Payload: None - - Note: This endpoint returns up to 100 of the most recent clicks. For complete click history, use the `/api/links/:id/clicks` endpoint with pagination. - - Response: 200 - - Response Example - ```json - { - "data": { - "id": "84f0c7a4-8c4e-4665-b676-cb9c5e40f1db", - "refer": "http://localhost:4000/3wP4BQ", - "origin": "https://monocuco.donado.co", - "clicks": [ - { - "id": "730e2202-58f9-478c-a24c-f1c561df6716", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0", - "country": "DE", - "browser": "Firefox", - "os": "Mac OS X", - "referer": "Direct", - "created_at": "2024-07-12T19:25:22Z" - } - ] - } - } - ``` - -5. **List Clicks for a Link** - - Endpoint: `GET /api/links/:id/clicks` - - Headers: `X-Api-Key` - - Query Parameters: - - `limit` (optional): Number of results per page (default: 100) - - `cursor` (optional): Pagination cursor from previous response - - Response: 200 - - Response Example - ```json - { - "data": [ - { - "id": "730e2202-58f9-478c-a24c-f1c561df6716", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0", - "country": "DE", - "browser": "Firefox", - "os": "Mac OS X", - "referer": "Direct", - "created_at": "2024-07-12T19:25:22Z" - } - ], - "pagination": { - "has_more": true, - "next": "629e3301-47f8-389b-b24c-f1c561df9825" - } - } - ``` - -6. **Create new link** - - Endpoint: `POST /api/links` - - Payload: - ```json - { - "url": "https://example.com" - } - ``` - - Headers: `X-Api-Key` - - Response: 201 - - Response Example: - ```json - { - "data": { - "id": "84f0c7a4-8c4e-4665-b676-cb9c5e40f1db", - "refer": "http://localhost:4000/3wP4BQ", - "origin": "https://example.com", - "clicks": [] - } - } - ``` - -7. **Update an existing link by ID** - - Endpoint: `PUT /api/links/:id` - - Payload: - ```json - { - "url": "https://newexample.com" - } - ``` - - Headers: `X-Api-Key` - - Response: 200 - - Response Example: - ```json - { - "data": { - "id": "84f0c7a4-8c4e-4665-b676-cb9c5e40f1db", - "refer": "http://localhost:4000/3wP4BQ", - "origin": "https://newexample.com", - "clicks": [] - } - } - ``` - -8. **Delete a link by ID** - - Endpoint: `DELETE /api/links/:id` - - Payload: None - - Headers: `X-Api-Key` - - Response: 204 diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..8a3d70f --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,496 @@ +openapi: 3.1.0 +info: + title: Bit - URL Shortener API + description: A high-performance URL shortener service with click tracking and analytics + version: 1.0.0 + contact: + name: API Support + url: https://github.com/sjdonado/bit + +servers: + - url: http://localhost:4000 + description: Development server + - url: http://localhost:4001 + description: Benchmark server + +security: + - ApiKeyAuth: [] + +paths: + /api/ping: + get: + summary: Ping the API + description: Health check endpoint to verify the API is running + operationId: ping + tags: + - Health + security: [] + responses: + '200': + description: API is healthy + content: + application/json: + schema: + type: object + properties: + data: + type: string + example: pong + + /{slug}: + get: + summary: Redirect by slug + description: Redirects to the original URL and tracks the click asynchronously + operationId: redirectBySlug + tags: + - Redirects + security: [] + parameters: + - name: slug + in: path + required: true + description: The short URL slug + schema: + type: string + example: 3wP4BQ + - name: utm_source + in: query + required: false + description: UTM source parameter for tracking + schema: + type: string + example: email_campaign + responses: + '301': + description: Redirect to original URL + headers: + Location: + description: The original URL + schema: + type: string + example: https://example.com + X-Forwarded-For: + description: Client IP address + schema: + type: string + User-Agent: + description: User agent string + schema: + type: string + '404': + description: Link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/links: + get: + summary: List all links + description: Retrieve all links for the authenticated user with pagination support + operationId: listLinks + tags: + - Links + parameters: + - name: limit + in: query + description: Number of results per page + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + - name: cursor + in: query + description: Pagination cursor from previous response + schema: + type: string + responses: + '200': + description: List of links + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/LinkSummary' + pagination: + $ref: '#/components/schemas/Pagination' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + post: + summary: Create new link + description: Create a new shortened link + operationId: createLink + tags: + - Links + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - url + properties: + url: + type: string + format: uri + description: The URL to shorten + example: https://example.com + responses: + '201': + description: Link created successfully + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Link' + '400': + description: Bad request - invalid URL or missing field + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + missingField: + value: + error: "url: Required field" + invalidUrl: + value: + errors: + url: + - is invalid + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/links/{id}: + get: + summary: Get link by ID + description: Retrieve a specific link with up to 100 most recent clicks. For complete click history, use /api/links/{id}/clicks + operationId: getLink + tags: + - Links + parameters: + - name: id + in: path + required: true + description: Link ID + schema: + type: integer + format: int64 + responses: + '200': + description: Link details + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Link' + '404': + description: Link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + summary: Update link + description: Update the URL of an existing link + operationId: updateLink + tags: + - Links + parameters: + - name: id + in: path + required: true + description: Link ID + schema: + type: integer + format: int64 + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - url + properties: + url: + type: string + format: uri + description: The new URL + example: https://newexample.com + responses: + '200': + description: Link updated successfully + content: + application/json: + schema: + type: object + properties: + data: + $ref: '#/components/schemas/Link' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - link belongs to another user + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + summary: Delete link + description: Delete a link and all its associated clicks + operationId: deleteLink + tags: + - Links + parameters: + - name: id + in: path + required: true + description: Link ID + schema: + type: integer + format: int64 + responses: + '204': + description: Link deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Forbidden - link belongs to another user + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /api/links/{id}/clicks: + get: + summary: List clicks for a link + description: Retrieve all clicks for a specific link with pagination support + operationId: listClicks + tags: + - Clicks + parameters: + - name: id + in: path + required: true + description: Link ID + schema: + type: integer + format: int64 + - name: limit + in: query + description: Number of results per page + schema: + type: integer + default: 100 + minimum: 1 + maximum: 1000 + - name: cursor + in: query + description: Pagination cursor from previous response + schema: + type: string + responses: + '200': + description: List of clicks + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Click' + pagination: + $ref: '#/components/schemas/Pagination' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Link not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-Api-Key + description: API key for authentication + + schemas: + LinkSummary: + type: object + properties: + id: + type: integer + format: int64 + description: Unique link identifier + example: 1 + refer: + type: string + format: uri + description: The shortened URL + example: http://localhost:4000/3wP4BQ + origin: + type: string + format: uri + description: The original URL + example: https://monocuco.donado.co + + Link: + allOf: + - $ref: '#/components/schemas/LinkSummary' + - type: object + properties: + clicks: + type: array + description: Array of click records (up to 100 most recent) + items: + $ref: '#/components/schemas/Click' + + Click: + type: object + properties: + id: + type: integer + format: int64 + description: Unique click identifier + example: 1 + user_agent: + type: string + description: User agent string + example: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:127.0) Gecko/20100101 Firefox/127.0 + country: + type: string + nullable: true + description: Country code (ISO 3166-1 alpha-2) + example: US + browser: + type: string + nullable: true + description: Browser name + example: Firefox + os: + type: string + nullable: true + description: Operating system + example: Mac OS X + referer: + type: string + nullable: true + description: Referer domain or utm_source + example: Direct + created_at: + type: string + format: date-time + description: Click timestamp + example: 2024-07-12T19:25:22Z + + Pagination: + type: object + properties: + has_more: + type: boolean + description: Whether there are more results + example: true + next: + type: integer + format: int64 + nullable: true + description: Cursor for next page (link/click ID) + example: 12 + + Error: + type: object + properties: + error: + type: string + description: Error message + example: Resource not found + required: + - error + + ValidationErrors: + type: object + properties: + errors: + type: object + additionalProperties: + type: array + items: + type: string + description: Field-level validation errors + example: + url: + - is invalid + +tags: + - name: Health + description: Health check endpoints + - name: Redirects + description: URL redirection and click tracking + - name: Links + description: Link management operations + - name: Clicks + description: Click analytics and tracking