Now that you have your development environment set up and have run a minimal FastAPI application, let's put theory into practice by creating several basic API endpoints. This hands-on exercise will solidify your understanding of path operations, HTTP methods, and how FastAPI handles requests.
We will continue working with the main.py
file introduced in the previous section. Remember to run your application using uvicorn main:app --reload
in your terminal. The --reload
flag is convenient during development as it automatically restarts the server when you save changes to your code.
The most common HTTP method is GET, used to retrieve data from a specified resource. In FastAPI, you define a GET endpoint using the @app.get()
decorator above an async
function.
Let's modify your main.py
to include a root GET endpoint that returns a simple JSON message:
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
"""
This endpoint returns a welcome message.
It serves as the root or index of the API.
"""
return {"message": "Welcome to the ML Model API"}
Here's a breakdown:
@app.get("/")
: This decorator tells FastAPI that the function read_root
below it is responsible for handling GET requests made to the path /
(the root path).async def read_root():
: This defines an asynchronous function named read_root
. FastAPI supports both synchronous (def
) and asynchronous (async def
) route handlers. Using async def
allows leveraging Python's asyncio
for concurrent operations, which we'll discuss more in Chapter 5. For now, know that it's the standard way to define path operation functions in FastAPI.return {"message": "Welcome to the ML Model API"}
: FastAPI automatically converts this Python dictionary into a JSON response.Save the file. If uvicorn
is running with --reload
, it should restart automatically. Open your web browser and navigate to http://127.0.0.1:8000/
. You should see the JSON response: {"message":"Welcome to the ML Model API"}
.
You can also access the automatically generated interactive API documentation by navigating to http://127.0.0.1:8000/docs
. This interface (Swagger UI) allows you to explore and test your API endpoints directly from the browser. Find the /
endpoint, expand it, and click "Try it out" then "Execute".
Often, you need to retrieve a specific item based on an identifier included in the URL path. These are called path parameters. Let's create an endpoint to retrieve information about a specific item ID.
# main.py (add this function below the previous one)
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"message": "Welcome to the ML Model API"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
"""
Retrieves an item based on its ID provided in the path.
"""
# In a real application, you would fetch data based on item_id
return {"item_id": item_id, "description": f"Details for item {item_id}"}
Key changes:
@app.get("/items/{item_id}")
: The path now includes {item_id}
. This signifies a path parameter named item_id
.async def read_item(item_id: int):
: The function now accepts an argument named item_id
. Crucially, we've added a type hint : int
. FastAPI uses this hint to:
item_id
can be converted to an integer.Save the file and test this endpoint. Go to http://127.0.0.1:8000/items/42
in your browser. You should see: {"item_id":42,"description":"Details for item 42"}
.
Now, try accessing http://127.0.0.1:8000/items/abc
. FastAPI automatically intercepts this request before it reaches your function code because "abc" cannot be converted to an integer. It returns a helpful JSON error response indicating the validation failure:
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
This automatic data validation is a major benefit of using FastAPI and type hints.
Query parameters are optional key-value pairs appended to the URL after a question mark (?
), often used for filtering, sorting, or pagination. Unlike path parameters, they are not part of the path structure itself.
Let's modify the /items/
endpoint (or create a new one) to accept optional skip
and limit
query parameters for pagination.
# main.py (modify or add as needed)
from fastapi import FastAPI
from typing import Optional # Import Optional
app = FastAPI()
# ... (previous endpoints) ...
@app.get("/users/")
async def read_users(skip: int = 0, limit: int = 10):
"""
Retrieves a list of users, with optional pagination.
Uses query parameters 'skip' and 'limit'.
"""
# Simulate fetching users from a data source
all_users = [{"user_id": i, "name": f"User {i}"} for i in range(100)]
return all_users[skip : skip + limit]
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Optional[str] = None):
"""
Retrieves an item by ID, with an optional query parameter 'q'.
"""
response = {"item_id": item_id, "description": f"Details for item {item_id}"}
if q:
response.update({"query_param": q})
return response
Explanation:
read_users(skip: int = 0, limit: int = 10)
:
skip
and limit
are defined as function arguments without being in the path string (/users/
). FastAPI recognizes them as query parameters.: int
) provide validation and conversion.= 0
, = 10
) make these parameters optional. If the client doesn't provide them in the URL, these defaults are used.read_item(item_id: int, q: Optional[str] = None)
:
q
.Optional[str]
indicates that q
can be a string or None
.= None
sets its default value to None
, making it optional.Test these:
http://127.0.0.1:8000/users/
: Uses defaults (skip=0
, limit=10
).http://127.0.0.1:8000/users/?skip=10&limit=5
: Uses provided values.http://127.0.0.1:8000/items/5
: No query parameter q
.http://127.0.0.1:8000/items/5?q=somequery
: Includes the query parameter q
.Again, try providing invalid types, like skip=abc
, to see the automatic validation errors.
The POST method is used to send data to the server to create or update a resource. Defining a POST endpoint is similar to GET, using the @app.post()
decorator.
Handling the body of a POST request (the data being sent) typically involves Pydantic models, which we'll cover in detail in Chapter 2. For this practice, let's create a simple POST endpoint that just acknowledges the request without processing complex input data yet.
# main.py (add this function)
from fastapi import FastAPI
from typing import Optional
app = FastAPI()
# ... (all previous endpoints) ...
@app.post("/items/")
async def create_item():
"""
Placeholder endpoint for creating a new item.
Currently just returns a confirmation message.
Data reception will be handled in Chapter 2.
"""
# In Chapter 2, we'll learn how to receive data here
return {"message": "Item received (but not processed yet)"}
Since browsers typically only make GET requests directly via the address bar, you'll need a different tool to test this POST endpoint easily:
http://127.0.0.1:8000/docs
, find the POST /items/
endpoint, expand it, click "Try it out", and then "Execute". You should see the success response.curl -X POST http://127.0.0.1:8000/items/ -H "accept: application/json"
The -X POST
flag specifies the HTTP method. The -H
adds a header indicating we accept JSON responses. You should receive {"message":"Item received (but not processed yet)"}
.Here is the complete main.py
incorporating all the endpoints we created:
# main.py
from fastapi import FastAPI
from typing import Optional
app = FastAPI(title="Simple API Practice", version="0.1.0")
@app.get("/")
async def read_root():
"""
Root endpoint returning a welcome message.
"""
return {"message": "Welcome to the ML Model API"}
@app.get("/users/")
async def read_users(skip: int = 0, limit: int = 10):
"""
Retrieves a list of users, with optional pagination.
Uses query parameters 'skip' and 'limit'.
"""
all_users = [{"user_id": i, "name": f"User {i}"} for i in range(100)]
return all_users[skip : skip + limit]
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Optional[str] = None):
"""
Retrieves an item by ID (path parameter), with an optional query parameter 'q'.
"""
response = {"item_id": item_id, "description": f"Details for item {item_id}"}
if q:
response.update({"query_param": q})
return response
@app.post("/items/")
async def create_item():
"""
Placeholder endpoint for creating a new item via POST.
"""
return {"message": "Item received (but not processed yet)"}
# To run: uvicorn main:app --reload
You have now successfully created endpoints using different HTTP methods (GET, POST) and learned how to handle path and query parameters with automatic validation provided by FastAPI's type hinting system. You also saw the utility of the interactive API documentation. In the next chapter, we will significantly enhance our ability to handle data, especially complex request bodies for POST requests, using Pydantic models.
© 2025 ApX Machine Learning