An API gives another program a predictable way to request data. Our first endpoint will return a task by its numeric ID. The goal is to understand the route, the input, and the response before introducing a database.
Create an isolated project
With Python 3 and uv installed, run these commands in a terminal. The standard FastAPI extras include the development command. Save the Python code in the next section as main.py inside the new folder.
uv init task-api cd task-api uv add "fastapi[standard]"
Define the route and its result
from fastapi import FastAPI, HTTPException app = FastAPI() tasks = {1: {"id": 1, "title": "Review the page", "done": False}} @app.get("/tasks/{task_id}") def read_task(task_id: int): task = tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="Task not found") return task
The path captures task_id and the int annotation tells FastAPI to parse it as an integer. A valid ID reaches the dictionary lookup. A missing task produces a 404 response. A value such as /tasks/hello fails input validation before the function runs.
Run it and inspect the contract
uv run fastapi dev main.py
Open http://127.0.0.1:8000/docs in your browser. Expand the GET endpoint and use Try it out with ID 1. Then try ID 999 and a nonnumeric value. Compare the successful JSON result, the missing-resource response, and the validation response. Stop the server with Ctrl+C when you finish.
Know what the example leaves out
The dictionary is temporary process memory. It disappears on restart, is not shared across workers, and is unsuitable as a production database. This endpoint also has no authentication. Before exposing private tasks, add identity checks and verify access to each requested record.
Input types do not establish business rules: an integer can still refer to a task owned by someone else. Keep that distinction clear as you add a database and response models. Use normal def for this simple synchronous example; async def becomes useful when the work inside can await asynchronous I/O.
Choose one next step
Add a response model that documents the task fields, then write checks for a known task, an unknown ID, and invalid input. Once those behaviors are stable, replace the dictionary with persistent storage. Build outward from one endpoint whose behavior you understand.
Add a repeatable endpoint test
Clicking through interactive documentation is useful while exploring, but it is easy to forget one failure case after a code change. Add automated checks for the three responses you already inspected. These tests call the application through a test client, so you do not need to start the development server first.
uv add --dev pytest httpx
Save this as test_main.py next to main.py. The known task check verifies the response body as well as its status. The other checks describe the difference between a valid identifier that is absent and a value that cannot be parsed as an identifier.
from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_known_task(): response = client.get("/tasks/1") assert response.status_code == 200 assert response.json() == { "id": 1, "title": "Review the page", "done": False } def test_unknown_task(): response = client.get("/tasks/999") assert response.status_code == 404 assert response.json()["detail"] == "Task not found" def test_invalid_identifier(): response = client.get("/tasks/hello") assert response.status_code == 422
uv run pytest
Test what a client depends on
Imagine a task dashboard that highlights unfinished work. A status-only test would still pass if your endpoint accidentally stopped returning done. Check the fields that drive the dashboard. Avoid asserting every word in framework-generated validation messages unless your client explicitly relies on them; a library update can improve that wording without changing the behavior you need.
The sample dictionary never changes during these GET tests, so every test sees the same starting state. Once you add endpoints that create or update tasks, give each test a fresh store or database transaction. A test that passes only after another test has run is a warning that state is leaking between examples.
Design the create operation before implementing it
For a POST /tasks endpoint, decide which fields the client supplies and which the server owns. The client might send title, while the server assigns id and sets done to false. Specify how blank titles are handled, whether repeated submissions create duplicates, and what the successful response contains. Writing those decisions first makes the implementation easier to review.
Sketch three examples before coding: a normal title, an empty title, and a request missing title. For each, record the intended status and body. Then introduce a request model and make those examples executable tests. Leave persistence until you can explain the behavior independently of the database technology.
Recognize development-server problems
If the server says the address is already in use, another process is listening on that port. Stop the process you started or choose another port; avoid terminating an unrelated application. If the command cannot locate main.py, confirm your current folder. If /docs loads but your endpoint is absent, verify that the decorator is attached to the same app object the server imports.
A browser request from a different origin introduces an additional concern: cross-origin policy. Confirm that the endpoint works directly before investigating the frontend connection. If you need CORS, configure the specific frontend origins your application uses. CORS does not replace authentication or record-level authorization.
Prepare for persistent storage
Move lookup behavior into a small service when the example grows. The route should translate an HTTP request into a service call and translate its result into a response. Keep storage details behind that boundary. This makes it possible to test the route with controlled data while testing database queries separately.
Before calling the project ready for real users, verify that tasks survive a restart, unauthorized users cannot read another user's tasks, expected errors have consistent responses, and logs avoid sensitive data. These are concrete acceptance checks you can repeat, rather than a vague promise that the API is production ready.
Comments