Skip to content

Quickstart: Web Backend

This guide walks you through building a small FastAPI service that exposes ERLC server data over HTTP. By the end you will have four endpoints: GET /status, GET /players, GET /staff, and POST /announce.

1. Prerequisites

  • Install the required packages:
    pip install erlc-api.py fastapi uvicorn
    
  • A PRC server key — see Clients and Authentication for how to obtain one.

2. Client lifecycle

Use FastAPI's lifespan context manager to start and close AsyncClient alongside the app. This replaces the deprecated @app.on_event("startup"/"shutdown") approach.

import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, Header, HTTPException
from erlc_api import AsyncClient, CommandPolicy, CommandPolicyError, cmd
from erlc_api.cache import AsyncCachedClient

api = AsyncClient.from_env()
cached_api = AsyncCachedClient(api, ttl_s=5)
announce_policy = CommandPolicy(allowed={"h"}, max_length=120)
admin_token = os.environ["ERLC_ADMIN_TOKEN"]


@asynccontextmanager
async def lifespan(app: FastAPI):
    await api.start()
    yield
    await api.close()


app = FastAPI(lifespan=lifespan)

3. Endpoints

GET /status — server overview

@app.get("/status")
async def status():
    info = await cached_api.server()
    return {
        "name": info.name,
        "players": info.current_players,
        "max_players": info.max_players,
    }

GET /players — list online players

@app.get("/players")
async def players():
    online = await cached_api.players()
    return [{"name": p.name, "team": p.team, "user_id": p.user_id} for p in online]

GET /staff — staff on duty

@app.get("/staff")
async def staff():
    duty = (await cached_api.staff()).members
    return [{"name": m.name, "role": str(m.role)} for m in duty]

POST /announce — broadcast a hint

from pydantic import BaseModel

class AnnounceBody(BaseModel):
    message: str

@app.post("/announce")
async def announce(body: AnnounceBody, x_admin_token: str | None = Header(default=None)):
    if x_admin_token != admin_token:
        raise HTTPException(status_code=403, detail="Command access denied.")
    try:
        safe_command = announce_policy.validate(cmd.h(body.message))
    except CommandPolicyError as exc:
        raise HTTPException(status_code=400, detail=exc.result.reason) from exc

    preview = await api.preview_command(safe_command, policy=announce_policy)
    result = await api.command(preview.command, policy=announce_policy)
    return {"ok": True, "command": preview.command, "message": result.message}

4. Error handling

Convert API errors to proper HTTP responses using HTTPException.

from erlc_api import AuthError, RateLimitError, ERLCError
from erlc_api.diagnostics import diagnose_error

@app.get("/status")
async def status():
    try:
        info = await cached_api.server()
        return {"name": info.name, "players": info.current_players, "max_players": info.max_players}
    except AuthError:
        raise HTTPException(status_code=401, detail="Invalid server key.")
    except RateLimitError as e:
        raise HTTPException(status_code=429, detail=diagnose_error(e).to_dict())
    except ERLCError as e:
        raise HTTPException(status_code=502, detail=diagnose_error(e).to_dict())

5. Dashboard helpers

Use status, bundle presets, and multi-server helpers for routes that feed dashboards.

from erlc_api.multiserver import AsyncMultiServer, ServerRef
from erlc_api.status import StatusBuilder

@app.get("/dashboard")
async def dashboard():
    bundle = await cached_api.bundle()
    return StatusBuilder(bundle).build().to_dict()

servers = [
    ServerRef("main", "main-server-key"),
    ServerRef("training", "training-server-key"),
]

@app.get("/servers")
async def servers_view():
    return await AsyncMultiServer(cached_api, servers, concurrency=3).aggregate()

AsyncCachedClient is intentionally read-only for caching. Keep POST endpoints such as /announce calling api.command(...) directly.

6. Running the server

uvicorn main:app --reload

The API will be available at http://localhost:8000. FastAPI generates interactive docs at /docs automatically.

7. Common mistakes

  • Using @app.on_event("startup"/"shutdown"). These are deprecated in modern FastAPI. Use the lifespan context manager instead.
  • Sharing one AsyncClient instance across threads. The client is not thread-safe; use it only within the async event loop FastAPI runs on.
  • Returning model objects directly. erlc_api models are dataclasses, not Pydantic models. Call .to_dict() or map fields manually before returning from a route.
  • Not handling RateLimitError. ERLC enforces per-endpoint limits. Without handling, FastAPI returns a 500 to the caller instead of a meaningful 429.
  • Starting the client outside lifespan. Calling await api.start() at module level runs before an event loop exists and will raise a runtime error.
  • Using api.server(all=True) for every route. Use bundle presets or explicit includes so hot paths only request what they need.
  • Caching write endpoints. Cache helpers skip command(...); keep command routes explicit.
  • Leaving command routes public. Protect them with authentication, CommandPolicy, cooldown/rate limits, and audit logs.

8. Next steps


Previous Page: Getting Started | Next Page: Quickstart: Discord.py