Start with boundaries, not routes
A production API is easier to maintain when HTTP handling is thin. Routes should validate input, call an application service and format the response. Database queries, cloud calls and business decisions belong behind interfaces that can be tested without running a web server.
Use Pydantic models as contracts at the API boundary. Keep persistence models separate when database structure and public output may evolve independently. This avoids exposing internal fields and makes versioned API changes far less risky.
from fastapi import APIRouter, Depends
router = APIRouter()
@router.post("/devices", response_model=DeviceView, status_code=201)
async def register_device(
command: CreateDevice,
service: DeviceService = Depends(get_device_service),
):
return await service.register(command)
Use async where waiting dominates
Async endpoints are useful when the request spends time waiting for network I/O: PostgreSQL drivers, Redis, object storage or external APIs. They do not magically make CPU-heavy image processing or analytics faster. CPU-bound work should move to a worker or a separate computation service.
- Choose async database and HTTP clients consistently across the request path.
- Set timeouts for every external call and return controlled errors.
- Use idempotency keys for operations that clients may retry, such as payment or provisioning requests.
Design the service for operations
Health endpoints need two meanings: a liveness signal that says the process runs, and a readiness signal that confirms dependencies required to serve traffic are available. Keep readiness checks bounded by strict timeouts so a failing database does not make the check itself hang.
Structured logs should carry request IDs, status codes, latency and relevant domain identifiers without secrets. Metrics should answer whether traffic, errors or latency changed. Traces become valuable once a request crosses services, queues or cloud functions.
- Configuration from environment or secret storage, never source code.
- Authentication and authorization tested at the boundary.
- Database migrations executed as a deliberate release step.
- Rate limiting, timeout handling and documented error responses.
Release with confidence
Package the service in a small container image, run unit and contract tests in CI, then deploy the identical image through environments. An OpenAPI document can become a testable contract for frontend or integration teams. When changes are risky, release behind a feature flag or shift traffic gradually.
A good API is not merely fast in a demo. It is observable, recoverable and easy for the next engineer to extend. That is the difference between creating endpoints and engineering a backend platform.