Files
thrillwiki_django_no_react/.agent/rules/api-conventions.md
pacnpal 1adba1b804 lol
2026-01-02 07:58:58 -05:00

6.1 KiB

ThrillWiki API Conventions

Standards for designing and implementing APIs between the Django backend and Nuxt frontend.

API Base Structure

URL Patterns

/api/v1/                     # API root (versioned)
/api/v1/parks/               # List/Create parks
/api/v1/parks/{slug}/        # Retrieve/Update/Delete park
/api/v1/parks/{slug}/rides/  # Nested resource
/api/v1/auth/                # Authentication endpoints
/api/v1/users/me/            # Current user

HTTP Methods

Method Usage
GET Retrieve resource(s)
POST Create resource
PUT Replace resource entirely
PATCH Update resource partially
DELETE Remove resource

Response Formats

Success Response (Single Resource)

{
  "id": "uuid",
  "name": "Cedar Point",
  "slug": "cedar-point",
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-16T14:20:00Z",
  ...
}

Success Response (List)

{
  "count": 150,
  "next": "/api/v1/parks/?page=2",
  "previous": null,
  "results": [
    { ... },
    { ... }
  ]
}

Error Response

{
  "error": {
    "code": "validation_error",
    "message": "Invalid input data",
    "details": {
      "name": ["This field is required."],
      "status": ["Invalid choice."]
    }
  }
}

HTTP Status Codes

Code Meaning When to Use
200 OK Successful GET, PUT, PATCH
201 Created Successful POST
204 No Content Successful DELETE
400 Bad Request Validation errors
401 Unauthorized Missing/invalid auth
403 Forbidden Insufficient permissions
404 Not Found Resource doesn't exist
409 Conflict Duplicate resource
500 Server Error Unexpected errors

Pagination

Query Parameters

?page=2           # Page number (default: 1)
?page_size=20     # Items per page (default: 20, max: 100)

Response

{
  "count": 150,
  "next": "/api/v1/parks/?page=3",
  "previous": "/api/v1/parks/?page=1",
  "results": [ ... ]
}

Filtering & Sorting

Filtering

GET /api/v1/parks/?status=operating
GET /api/v1/parks/?country=USA&status=operating
GET /api/v1/rides/?park=cedar-point&type=coaster
GET /api/v1/parks/?search=cedar

Sorting

GET /api/v1/parks/?ordering=name          # Ascending
GET /api/v1/parks/?ordering=-created_at   # Descending
GET /api/v1/parks/?ordering=-rating,name  # Multiple fields

Authentication

Token-Based Auth

Authorization: Bearer <jwt_token>

Endpoints

POST /api/v1/auth/login/          # Get tokens
POST /api/v1/auth/register/       # Create account
POST /api/v1/auth/refresh/        # Refresh access token
POST /api/v1/auth/logout/         # Invalidate tokens
GET  /api/v1/auth/me/             # Current user info

Social Auth

POST /api/v1/auth/google/         # Google OAuth
POST /api/v1/auth/discord/        # Discord OAuth

Content Submission API

Submit New Content

POST /api/v1/submissions/
{
  "content_type": "park",
  "data": {
    "name": "New Park Name",
    "city": "Orlando",
    ...
  }
}

Response

{
  "id": "submission-uuid",
  "status": "pending",
  "content_type": "park",
  "data": { ... },
  "created_at": "2024-01-15T10:30:00Z"
}

Submit Edit to Existing Content

POST /api/v1/submissions/
{
  "content_type": "park",
  "object_id": "existing-park-uuid",
  "data": {
    "description": "Updated description..."
  }
}

Moderation API

Get Moderation Queue

GET /api/v1/moderation/
GET /api/v1/moderation/?status=pending&type=park

Review Submission

POST /api/v1/moderation/{id}/approve/
POST /api/v1/moderation/{id}/reject/
{
  "notes": "Reason for rejection..."
}

Nested Resources

Pattern

/api/v1/parks/{slug}/rides/              # List rides in park
/api/v1/parks/{slug}/reviews/            # List park reviews
/api/v1/parks/{slug}/photos/             # List park photos

When to Nest vs Flat

Nest when:

  • Resource only makes sense in context of parent (park photos)
  • Need to filter by parent frequently

Use flat with filter when:

  • Resource can exist independently
  • Need to query across parents
# Flat with filter
GET /api/v1/rides/?park=cedar-point

# Or nested
GET /api/v1/parks/cedar-point/rides/

Geolocation API

Parks Nearby

GET /api/v1/parks/nearby/?lat=41.4821&lng=-82.6822&radius=50

Response includes distance

{
  "results": [
    {
      "id": "uuid",
      "name": "Cedar Point",
      "distance": 12.5,  // in user's preferred units
      ...
    }
  ]
}

File Uploads

Photo Upload

POST /api/v1/photos/
Content-Type: multipart/form-data

{
  "content_type": "park",
  "object_id": "park-uuid",
  "image": <file>,
  "caption": "Optional caption"
}

Response

{
  "id": "photo-uuid",
  "url": "https://cdn.thrillwiki.com/photos/...",
  "thumbnail_url": "https://cdn.thrillwiki.com/photos/.../thumb",
  "status": "pending",  // Pending moderation
  "created_at": "2024-01-15T10:30:00Z"
}

Rate Limiting

Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1642089600

When Limited

HTTP/1.1 429 Too Many Requests
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Try again in 60 seconds."
  }
}

Frontend Integration

API Client Setup (Nuxt)

// composables/useApi.ts
export function useApi() {
  const config = useRuntimeConfig()
  const authStore = useAuthStore()
  
  const api = $fetch.create({
    baseURL: config.public.apiBase,
    headers: authStore.token ? {
      Authorization: `Bearer ${authStore.token}`
    } : {},
    onResponseError: ({ response }) => {
      if (response.status === 401) {
        authStore.logout()
        navigateTo('/auth/login')
      }
    }
  })
  
  return api
}

Usage in Components

const api = useApi()

// Fetch parks
const { data: parks } = await useAsyncData('parks', () =>
  api('/parks/', { params: { status: 'operating' } })
)

// Create submission
await api('/submissions/', {
  method: 'POST',
  body: { content_type: 'park', data: formData }
})