docs: document api usage
This commit is contained in:
147
README.md
Normal file
147
README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Pokedex API
|
||||
|
||||
REST API for the TrueLayer Software Engineering Challenge. It returns basic Pokémon information from PokéAPI and, on request, a fun translated description from FunTranslations.
|
||||
|
||||
## Requirements
|
||||
|
||||
Install these tools first:
|
||||
|
||||
- Rust, via `rustup`, for local development and tests.
|
||||
- `make`, for the documented command shortcuts.
|
||||
- Docker, for the containerized run path.
|
||||
|
||||
You can run it either with Docker or with a local Rust toolchain. Common commands are available through `make`; run `make help` for the full list.
|
||||
|
||||
### Option A: Docker
|
||||
|
||||
Install Docker Desktop or an equivalent Docker runtime, then run:
|
||||
|
||||
```bash
|
||||
make docker-build
|
||||
make docker-run
|
||||
```
|
||||
|
||||
### Option B: local Rust
|
||||
|
||||
Install Rust with rustup:
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source "$HOME/.cargo/env"
|
||||
rustup default stable
|
||||
```
|
||||
|
||||
Then run the service:
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
The API listens on `0.0.0.0:8000` by default.
|
||||
|
||||
## Endpoints
|
||||
|
||||
```bash
|
||||
make health
|
||||
make pokemon
|
||||
make translated
|
||||
make metrics
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mewtwo",
|
||||
"description": "It was created by a scientist after years of horrific gene splicing and DNA engineering experiments.",
|
||||
"habitat": "rare",
|
||||
"isLegendary": true
|
||||
}
|
||||
```
|
||||
|
||||
## Translation rules
|
||||
|
||||
`/pokemon/translated/{name}` applies Yoda when the Pokémon is legendary or its habitat is `cave`; otherwise it applies Shakespeare. If translation fails or returns an empty result, the API falls back to the standard PokéAPI description.
|
||||
|
||||
# Architecture
|
||||
|
||||
This is a small Rust service, so the structure is intentionally a lightweight hexagonal/onion layout rather than a framework-heavy one.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
HTTP[HTTP layer: axum routes, handlers, middleware] --> Application
|
||||
Application[Application service: use cases and translation rules] --> Domain
|
||||
Application --> Clients
|
||||
Clients[External clients: PokéAPI and FunTranslations]
|
||||
HTTP --> Telemetry
|
||||
Application --> Telemetry
|
||||
```
|
||||
|
||||
## Layers
|
||||
|
||||
- `domain`: core data and business concepts (`PokemonInfo`, `TranslationKind`).
|
||||
- `application`: orchestration and business rules, including Yoda vs Shakespeare selection and fallback behavior.
|
||||
- `clients`: adapters for external providers (`PokéAPI`, `FunTranslations`).
|
||||
- `http`: API transport concerns, routing, handlers, request IDs, metrics middleware, and rate limiting.
|
||||
- `telemetry`: OpenTelemetry metrics and tracing setup.
|
||||
- `bin`: executable entrypoint for the API.
|
||||
|
||||
## Deliberate trade-offs
|
||||
|
||||
The application service currently depends on concrete clients instead of trait-based ports. For this challenge that keeps the code easier to read and avoids abstractions that do not buy much yet. In a larger enterprise codebase, those clients would likely become traits owned by the application layer, with HTTP clients as adapters, so providers could be swapped or mocked without depending on concrete implementations.
|
||||
|
||||
Rate limiting is intentionally simple and in-memory. In production, this should usually live in an API gateway or shared rate-limiting service so limits are consistent across instances and can be keyed by API key, user, or client IP. PokéAPI species data also changes rarely, so production deployments would add a bounded LRU cache with TTL for successful species responses to avoid unnecessary random upstream hits, reduce latency, and make transient provider failures less visible to users.
|
||||
|
||||
External contract tests are separated from the normal deterministic test suite. Unit and integration tests use mocks; the nightly workflow calls real providers to detect contract drift early.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------- | ------------------------------------------- | -------------------------- |
|
||||
| `BIND_ADDR` | `0.0.0.0:8000` | HTTP bind address |
|
||||
| `POKEAPI_BASE_URL` | `https://pokeapi.co/api/v2` | PokéAPI base URL |
|
||||
| `FUN_TRANSLATIONS_BASE_URL` | `https://api.funtranslations.mercxry.me/v1` | FunTranslations base URL |
|
||||
| `REQUEST_TIMEOUT_SECONDS` | `5` | Upstream HTTP timeout |
|
||||
| `RATE_LIMIT_PER_SECOND` | `20` | Global token refill rate |
|
||||
| `RATE_LIMIT_BURST` | `40` | Global burst capacity |
|
||||
| `OTEL_SERVICE_NAME` | `pokedex-api` | OpenTelemetry service name |
|
||||
| `RUST_LOG` | `pokedex_api=info,tower_http=info` | JSON tracing log filter |
|
||||
|
||||
## Instrumentation
|
||||
|
||||
The service includes:
|
||||
|
||||
- structured JSON logging via `tracing`
|
||||
- request spans via `tower-http`
|
||||
- `x-request-id` generation and propagation
|
||||
- OpenTelemetry metrics for HTTP requests, HTTP errors, upstream calls, upstream errors and latency histograms
|
||||
- `/metrics` in Prometheus text format backed by OpenTelemetry instruments
|
||||
- simple global token-bucket rate limiting returning `429`
|
||||
|
||||
## Development checks
|
||||
|
||||
```bash
|
||||
make check
|
||||
```
|
||||
|
||||
The crate denies unsafe code and enables strict Clippy groups: `all`, `pedantic`, `nursery` and `cargo`, with only dependency-version noise allowed.
|
||||
|
||||
## External contract checks
|
||||
|
||||
Normal tests mock third-party APIs. A scheduled GitHub Actions workflow runs ignored contract tests nightly against the real PokéAPI and FunTranslations APIs to catch provider contract drift early:
|
||||
|
||||
```bash
|
||||
make contract-test
|
||||
```
|
||||
|
||||
## Docker multi-arch build
|
||||
|
||||
```bash
|
||||
make docker-buildx
|
||||
```
|
||||
|
||||
The Dockerfile uses `cargo-chef` and BuildKit cache mounts for dependency and target directory caching.
|
||||
|
||||
## Production notes
|
||||
|
||||
For a production API I would add retries with bounded exponential backoff, circuit breakers for upstream failures, stronger health checks that distinguish readiness from liveness, dashboards and alerts from the OTEL metrics, and a dedicated OTEL collector pipeline. I would keep `/metrics` private, add dependency/container scanning, define SLOs, and add contract tests against recorded upstream fixtures.
|
||||
170
openapi.yaml
Normal file
170
openapi.yaml
Normal file
@@ -0,0 +1,170 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Pokedex API
|
||||
version: 0.1.0
|
||||
description: REST API for the TrueLayer Pokémon challenge.
|
||||
servers:
|
||||
- url: http://localhost:8000
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: Health check
|
||||
responses:
|
||||
"200":
|
||||
description: Service is alive
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HealthResponse"
|
||||
/pokemon/{name}:
|
||||
get:
|
||||
summary: Return basic Pokémon information
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PokemonName"
|
||||
responses:
|
||||
"200":
|
||||
description: Pokémon information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PokemonInfo"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/PokemonNotFound"
|
||||
"502":
|
||||
$ref: "#/components/responses/UpstreamFailure"
|
||||
"429":
|
||||
$ref: "#/components/responses/RateLimited"
|
||||
/pokemon/translated/{name}:
|
||||
get:
|
||||
summary: Return Pokémon information with a fun translated description
|
||||
description: Uses Yoda for legendary or cave Pokémon, Shakespeare otherwise. Falls back to the standard description when translation fails.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/PokemonName"
|
||||
responses:
|
||||
"200":
|
||||
description: Pokémon information with translated or fallback description
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PokemonInfo"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"404":
|
||||
$ref: "#/components/responses/PokemonNotFound"
|
||||
"502":
|
||||
$ref: "#/components/responses/UpstreamFailure"
|
||||
"429":
|
||||
$ref: "#/components/responses/RateLimited"
|
||||
/metrics:
|
||||
get:
|
||||
summary: OpenTelemetry metrics in Prometheus text format
|
||||
responses:
|
||||
"200":
|
||||
description: Prometheus text exposition
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
components:
|
||||
parameters:
|
||||
PokemonName:
|
||||
name: name
|
||||
in: path
|
||||
required: true
|
||||
description: Pokémon identifier. Only ASCII letters, digits, and hyphens are accepted.
|
||||
schema:
|
||||
type: string
|
||||
pattern: "^[A-Za-z0-9-]+$"
|
||||
example: mewtwo
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
code: bad_request
|
||||
message: pokemon name must contain only ASCII letters, digits, or hyphens
|
||||
requestId: 7b2d6f7b-78ef-4cc3-85fd-5f09145fbd18
|
||||
PokemonNotFound:
|
||||
description: Pokémon was not found by PokéAPI
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
code: pokemon_not_found
|
||||
message: pokemon not found
|
||||
requestId: 7b2d6f7b-78ef-4cc3-85fd-5f09145fbd18
|
||||
UpstreamFailure:
|
||||
description: An upstream provider failed or returned invalid data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
examples:
|
||||
unavailable:
|
||||
value:
|
||||
code: upstream_unavailable
|
||||
message: upstream service failed
|
||||
requestId: 7b2d6f7b-78ef-4cc3-85fd-5f09145fbd18
|
||||
invalidData:
|
||||
value:
|
||||
code: upstream_invalid_data
|
||||
message: upstream service returned invalid data
|
||||
requestId: 7b2d6f7b-78ef-4cc3-85fd-5f09145fbd18
|
||||
RateLimited:
|
||||
description: Request was rate limited
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
example:
|
||||
code: rate_limit_exceeded
|
||||
message: rate limit exceeded
|
||||
requestId: 7b2d6f7b-78ef-4cc3-85fd-5f09145fbd18
|
||||
schemas:
|
||||
HealthResponse:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
const: ok
|
||||
PokemonInfo:
|
||||
type: object
|
||||
required: [name, description, habitat, isLegendary]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: mewtwo
|
||||
description:
|
||||
type: string
|
||||
example: It was created by a scientist after years of horrific gene splicing and DNA engineering experiments.
|
||||
habitat:
|
||||
type: string
|
||||
example: rare
|
||||
isLegendary:
|
||||
type: boolean
|
||||
example: true
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
enum:
|
||||
- bad_request
|
||||
- pokemon_not_found
|
||||
- upstream_invalid_data
|
||||
- upstream_unavailable
|
||||
- rate_limit_exceeded
|
||||
- internal_error
|
||||
message:
|
||||
type: string
|
||||
requestId:
|
||||
type: string
|
||||
description: Request correlation id from x-request-id.
|
||||
Reference in New Issue
Block a user