openapi: 3.0.3
info:
  title: Simon Cullen Portfolio API
  description: |
    A production-ready REST API demonstrating enterprise-level engineering practices.
    
    ## Features
    - **Rate Limiting**: Prevents abuse with configurable limits per endpoint
    - **Input Validation**: Server-side validation with character restrictions
    - **Input Sanitization**: XSS protection via HTML escaping
    - **CORS Protection**: Restricted to specific allowed origins
    - **Security Headers**: CSP, X-Frame-Options, X-XSS-Protection
    
    ## Authentication
    This API does not require authentication. Rate limiting is applied per IP address.
  version: 1.0.0
  contact:
    name: Simon Cullen
    url: https://simon-cullen.com
    email: contact@simon-cullen.com
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: https://simon-cullen.com/api
    description: Production server
  - url: http://localhost:9009/api
    description: Local development server

tags:
  - name: Health
    description: Service health monitoring
  - name: Entries
    description: User entry management (guestbook functionality)

paths:
  /health:
    get:
      tags:
        - Health
      summary: Health check
      description: Returns the current health status of the API service. Used for monitoring and uptime verification.
      operationId: getHealth
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: healthy
                  timestamp:
                    type: string
                    format: date-time
                    example: '2025-06-06T19:39:23Z'
                  service:
                    type: string
                    example: simoncullen-api
              example:
                status: healthy
                timestamp: '2025-06-06T19:39:23Z'
                service: simoncullen-api

  /entries:
    get:
      tags:
        - Entries
      summary: List recent entries
      description: |
        Retrieves recent entries from the guestbook. Results are ordered by most recent first.
        
        **Rate Limit**: 10 requests per 10 seconds per IP address.
      operationId: getEntries
      parameters:
        - name: limit
          in: query
          description: Maximum number of entries to return (1-50)
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 50
            default: 10
      responses:
        '200':
          description: Successful response with entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEntriesResponse'
              example:
                success: true
                message: Entries retrieved successfully
                data:
                  - id: abc123def456
                    name: John Doe
                    favorite_food: Pizza
                    timestamp: '2025-06-06T19:39:23.173294766Z'
                  - id: xyz789ghi012
                    name: Jane Smith
                    favorite_food: Sushi
                    timestamp: '2025-06-06T18:22:10.123456789Z'
                total: 2
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags:
        - Entries
      summary: Create a new entry
      description: |
        Creates a new guestbook entry with a name and favorite food.
        
        **Rate Limit**: 1 request per 5 seconds per IP address.
        
        **Validation Rules**:
        - Name: 2-50 characters, letters/numbers/spaces/basic punctuation only
        - Favorite Food: 2-50 characters, same character restrictions
        - Maximum 100 total entries (auto-reset every 4 hours)
      operationId: createEntry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEntryRequest'
            examples:
              valid:
                summary: Valid entry
                value:
                  name: John Doe
                  favorite_food: Pizza
              international:
                summary: International characters
                value:
                  name: François Müller
                  favorite_food: Crème brûlée
      responses:
        '201':
          description: Entry created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEntryResponse'
              example:
                success: true
                message: Entry created successfully
                data:
                  id: abc123def456
                  name: John Doe
                  favorite_food: Pizza
                  timestamp: '2025-06-06T19:39:23.173294766Z'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                validation_error:
                  summary: Validation error
                  value:
                    success: false
                    message: 'Invalid input: Name is required'
                invalid_chars:
                  summary: Invalid characters
                  value:
                    success: false
                    message: 'Invalid characters in input. Only letters, numbers, spaces and .,''- are allowed.'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          description: Rate limit or entry limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                rate_limit:
                  summary: Rate limit exceeded
                  value:
                    success: false
                    message: 'Rate limit exceeded. Please wait 5 seconds between requests.'
                    error: too_many_requests
                entry_limit:
                  summary: Maximum entries reached
                  value:
                    success: false
                    message: 'Maximum number of entries reached (100). Please try again later.'
        '500':
          $ref: '#/components/responses/InternalError'

  /entries/{id}:
    get:
      tags:
        - Entries
      summary: Get entry by ID
      description: |
        Retrieves a specific entry by its unique identifier.
        
        **Rate Limit**: 10 requests per 10 seconds per IP address.
      operationId: getEntryById
      parameters:
        - name: id
          in: path
          description: Unique entry identifier
          required: true
          schema:
            type: string
            pattern: '^[a-f0-9]{16}$'
            example: abc123def456
      responses:
        '200':
          description: Entry found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserEntryResponse'
              example:
                success: true
                message: Entry retrieved successfully
                data:
                  id: abc123def456
                  name: John Doe
                  favorite_food: Pizza
                  timestamp: '2025-06-06T19:39:23.173294766Z'
        '404':
          description: Entry not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                message: Entry not found
        '429':
          $ref: '#/components/responses/RateLimitExceeded'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  schemas:
    UserEntry:
      type: object
      description: A user entry in the guestbook
      properties:
        id:
          type: string
          description: Unique identifier (16 character hex string)
          example: abc123def456
          readOnly: true
        name:
          type: string
          description: User's name
          minLength: 2
          maxLength: 50
          example: John Doe
        favorite_food:
          type: string
          description: User's favorite food
          minLength: 2
          maxLength: 50
          example: Pizza
        timestamp:
          type: string
          format: date-time
          description: Creation timestamp (ISO 8601)
          example: '2025-06-06T19:39:23.173294766Z'
          readOnly: true

    CreateEntryRequest:
      type: object
      required:
        - name
        - favorite_food
      properties:
        name:
          type: string
          description: User's name (2-50 characters)
          minLength: 2
          maxLength: 50
          example: John Doe
        favorite_food:
          type: string
          description: User's favorite food (2-50 characters)
          minLength: 2
          maxLength: 50
          example: Pizza

    UserEntryResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: Entry created successfully
        data:
          $ref: '#/components/schemas/UserEntry'

    UserEntriesResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        message:
          type: string
          example: Entries retrieved successfully
        data:
          type: array
          items:
            $ref: '#/components/schemas/UserEntry'
        total:
          type: integer
          description: Total number of entries in the database
          example: 42

    ErrorResponse:
      type: object
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
          example: Invalid input
        error:
          type: string
          description: Error code (optional)
          example: too_many_requests

  responses:
    RateLimitExceeded:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            message: 'Rate limit exceeded. Please wait before requesting again.'
            error: too_many_requests

    PayloadTooLarge:
      description: Request payload exceeds size limit
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            message: Request entity too large

    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            message: 'Failed to process request'

  securitySchemes:
    rate_limit:
      type: apiKey
      description: Rate limiting is applied per IP address. No authentication required.

security:
  - {}

x-rate-limiting:
  default:
    requests: 10
    period: 10s
  post_entries:
    requests: 1
    period: 5s
    message: 'Rate limit exceeded. Please wait 5 seconds between requests.'

x-security-notes:
  - Input sanitization via bluemonday (XSS prevention)
  - Character validation (letters, numbers, spaces, basic punctuation only)
  - Request size limit: 1MB
  - Entry limit: 100 total (auto-reset every 4 hours)
  - Trusted proxy configuration for IP extraction
  - CORS restricted to specific origins