FastAPI Tutorial (Part 2) - Routes

12 min read
Table of Contents
- Introduction
- Tutorial Series Contents
- Project files and repo
- FastAPI Global Config
- Use Global Settings
- Books CRUD Routes
- Path Parameters
- Get Single book
- Finish the CRUD Routes
- Query parameters
- Authors Endpoints
- CORS (Cross-Origin Resource Sharing)
- Adding allowed origins
- Add list of origins to the app
- Quick Recap
- Next steps
Introduction
In the last post we created a base setup for our project. In case you didn’t read it, or want to check it, here is the link.
In this post we’re going to focus on defining our API endpoints (routes operations) and sending and receiving data from the client using url and query parameters. We’ll also see how to use Pydantic to add a global configuration to our application.
At the end, we’ll fix the CORS origin error to allow front-end applications to interact with our API.
Tutorial Series Contents
- Part 1: The Basics - Getting started, create base endpoints.
- Part 2: Routes - CRUD operations, Url and query params & returning data to the client, CORS.
- Part 3: Data Validation - Pydantic schemas, response models and error handling. (comming soon…)
- Part 4: Databases - SQL Databases and ORM’s (SQLAlchemy). (comming soon…)
- Part 5: Authentication - User authentication via JWT (register, login, logout password reset). (comming soon…)
Project files and repo
All the files for this and the other sections are stored in this repo. Each part of the series has a corresponding branch and the main
branch includes the finished project.
There is also a starter
branch with a base starting project, you can clone it or download it in case you want to follow along.
FastAPI Global Config
FastAPI uses a few main libraries under the hood to make development easier. One of those libraries is Pydantic. In case you don’t know, Pydantic is a Python library made to help with data validation using python type annotations.
We’re going to use a lot of Pydantic through the series to validate data, but, another of Pydantic main features is to manage project global settings.
To create a global config in our application, for this, let’s update our project structure to look like this:
fastapi-tutorial/
├── .vscode/
├── app/
│ ├── core/ --> NEW
│ │ ├── **init**.py --> NEW
│ │ └── config.py --> NEW
│ ├── endpoints/
│ │ ├── **init**.py
│ │ ├── api.py
│ │ └── books.py
│ ├── **init**.py
│ └── main.py
├── .gitignore
├── main.py
└── pyproject.toml
We create a new core
folder in which we can manage all global related settings. Inside this folder, we add a new config.py
file to create our settings.
To create global settings all we have to do is create a new Settings
class and extend Pydantic’s BaseSettings
, and then instantiate this class in a new settings
variable. We have the following:
from pydantic import BaseSettings
class Settings(BaseSettings):
"""Defines project global settings"""
API_STR: str = "/api"
PROJECT_TITLE: str = "Online Book Store API"
PROJECT_DESCRIPTION: str = "Backend API for an online Book Store"
settings = Settings()
Inside this new class we can add all the global configuration we need/want (we’ll add other properties to the class later). For the moment we’re creating 3 new configurations and adding string (str
) types to help with editor intellisense and avoid errors. You can use any title and description you like (this will be displayed in the API docs).
The API_STR
, refers to the API base endpoint. In the previous post we added this directly to the api_router
config since we didn’t have this settings at the moment. By doing it like this we can add versioning to our API more easy (e.g. /api/v1
), among other advantages. For simplicity, we’re not adding versioning to this project.
Use Global Settings
To use our new global settings, in the main.py
file, add this:
# from fastapi import FastAPI
# from app.endpoints.api import api_router
from app.core.config import settings
app = FastAPI(title=settings.PROJECT_TITLE, description=settings.PROJECT_DESCRIPTION)
app.include_router(api_router, prefix=settings.API_STR)
# @app.get("/")
# def index():
# return {"message": "Hello World"}
Now, go to your API docs.

As you can see, our API title has changed and a new description was added.
Books CRUD Routes
In the last post we created a simple GET
route to retrieve dummy data from a python list. Now let’s add the rest of routes for reading, updating, deleting and retrieving a single book.
Path Parameters
The first route we’ll add is to GET
a single book from the API using the book ID. To do this, we need to get the book ID from the URL. FastAPI makes this process simple, all we have to do is add the variable between curly brackets in the route decorator and then accept this variable as a parameter in the route function (operation) as follow:
# code above omitted 👆
@router.get("/{book_id}")
def get_book_by_id(book_id):
pass
By adding the book_id
to the path, FastAPI will read anything that goes after http://localhost:8000/api/books/
as a book ID, and then we accept this ID in the path operation.
Of course at the moment, python doesn’t know what type is this variable, so it can’t help us with any validation. To fix this, we can tell FastAPI that this ID should be of type int
(integer), that way FastAPI will try to convert any string passed to this endpoint to an integer and if it can’t then will raise a new exception.
# code above omitted 👆
@router.get("/{book_id}")
def get_book_by_id(book_id: int):
pass
Get Single book
Since we’re not persisting data yet, at this point we can use any python way to get an item from a list. For this case I’m going to loop through the list since it’s just 3 items. Later when we add a database we’ll improve this code.
# code above omitted 👆
@router.get("/{book_id}")
def get_book_by_id(book_id: int):
"""Get single book from database"""
for book in books:
if book["id"] == book_id:
return book
# code above bellow 👇
Again, this may not be the most efficient way, but for the moment it will work. We’ll improve it later.
Finish the CRUD Routes
The same way as before, I’m going to create a route for POST
, PUT
, and DELETE
. We end up with the following books.py
file:
from fastapi import APIRouter
from fastapi.encoders import jsonable_encoder
router = APIRouter()
books = [
{"id": 1, "title": "book 1", "price": 16.99},
{"id": 2, "title": "book 2", "price": 12.99},
{"id": 3, "title": "book 3", "price": 9.99},
]
@router.get("/")
def get_books():
"""Get books from database"""
return books
@router.post("/")
def add_book(new_book):
"""Create new book and store in database"""
books.append(new_book)
return books
@router.get("/{book_id}")
def get_book_by_id(book_id: int):
"""Get single book from database"""
for book in books:
if book["id"] == book_id:
return book
@router.put("/{book_id}")
def update_book(book_id: int, book):
"""Update book in database"""
update_book_encoded = jsonable_encoder(book)
books[book_id] = update_book_encoded
return update_book_encoded
@router.delete("/{book_id}")
def delete_book(book_id: int):
"""Delete book from database"""
for book in books:
if book["id"] == book_id:
books.pop(book_id - 1)
return books
Notice the update endpoint. We’re receiving two parameters, the book_id
and the book
object. The problem is, at the point FastAPI doesn’t know how does the book
object should look. If you go to your API docs, will notice that we currently can pass any data here that can be converted to a string.
You can also notice that if we try the POST
endpoint the client can send any data and the API will add it to the books
list without any type of validation. Of course, we don’t want that.
We’ll fix all of this later by adding a Pydantic Schema to tell FastAPI how this objects should look.
To update books, we’re using a function provided by FastAPI jsonable_encoder
. Basically this
function will convert a Pydantic schema into JSON format.
At this point we have the complete CRUD
operations for our books. In the next post, we’ll add
validation schemas to allow FastAPI help us handle the data we receive from the client and send
back.
Query parameters
When we create an API, it’s typical to allow the client to add extra parameters to the endpoint. A common example is a query for a specific range of values or adding pagination.
Since adding a variable between curly brackets to the path will make FastAPI see this as a path parameter, then the question is, how do we tell FastAPI about any additional query parameters.
Actually this is a very easy process. All we have to do is accept this query parameters as function parameters inside our path operation. FastAPI will see this params and check if they are part of the endpoint path, and if not, then FastAPI will interpret them as query parameters.
Let’s take pagination as an example. To add pagination to our project, the client will send a request to a route with something like this: http://localhost:8000/api/books?limit=1&skip=0
. This will ask to get at most one result per query (limit=1
), and skip zero results (skip=0
).
To allow this in our endpoint, all we have to do is pass this two params (limit
& skip
) ass our function params:
# code above omitted 👆
@router.get("/")
def get_books(limit: int = 1, skip: int = 0):
"""Get books from database"""
response = books[skip : skip + limit]
return response
# code above bellow 👇
If you refresh your browser, you can see two new fields in your docs (skip & limit).

Since we’re adding default values, you can test your API and notice that now you only get 1 result instead of all the books in the dummy list (3).

Authors Endpoints
Now that we have our books endpoints “ready”. Let’s create endpoints to get authors info. This endpoints will be useful later to create relationships between books and authors.
Let’s create the endpoints and register the router the same way we did with the books. We have the following authors.py
inside the endpoints/
folder.
from fastapi import APIRouter
router = APIRouter()
authors = [
{"id": 1, "name": "author 1", "bio": "author bio"},
{"id": 2, "name": "author 2", "bio": "author bio"},
{"id": 3, "name": "author 3", "bio": "author bio"},
]
@router.get("/")
def get_authors(limit: int = 1, skip: int = 0):
"""Get authors from database"""
response = authors[skip : skip + limit]
return response
@router.post("/")
def add_author(new_author):
"""Create new author and store in database"""
authors.append(new_author)
return authors
@router.get("/{author_id}")
def get_author_by_id(author_id: int):
"""Get single author from database"""
for author in authors:
if author["id"] == author_id:
return author
@router.put("/{author_id}")
def update_author(author_id: int):
"""Update author in database"""
return {"data": "updated author"}
@router.delete("/{author_id}")
def delete_author(author_id: int):
"""Delete author from database"""
for author in authors:
if author["id"] == author_id:
authors.pop(author_id - 1)
return authors
Later we’ll add other endpoints, but for the moment this will work.
Register this new router by updating the main api_router
inside the api.py
file.
# from fastapi import APIRouter
from . import books, authors
# api_router = APIRouter()
# api_router.include_router(books.router, prefix="/books", tags=["Books"])
api_router.include_router(authors.router, prefix="/authors", tags=["Authors"])
In your API docs you should see something like this:

The new authors
endpoints (just as any project endpoints) depend on what would you want to allow the client to do. Maybe you don’t want the client to delete authors or create a new author, in those cases you can omit this routes.
In this example we’re allowing all operations just for demostration.
CORS (Cross-Origin Resource Sharing)
At this point, you main want to test your endpoints from a frontend client application. If you’re building a full-stack application, for example using React, Vue or Svelte, you may encounter a situation in which your client application cannot communicate with your API.
This is because your client and your server are running in different ports (e.g. a React project running on port 3000
and your API running on port 8000
). This is what’s called CORS or “Cross-Origin Resource Sharing”.
To fix this, we have to tell our API, which origins are allowed to communicate with our server. You can do this by “blacklisting” (listing all the origins that are not allowed), or “whitelisting” (listing only the allowed origins).
Of course you can allow all origins by using *
as a wildcard (useful if your API is public).
Adding allowed origins
In our case we will only list which origins are allowed. To do this, in our global config file config.py
, we can add a list of allowed origins.
from typing import List
from pydantic import AnyHttpUrl, BaseSettings
# class Settings(BaseSettings):
# """Defines project global settings"""
# API_STR: str = "/api"
# PROJECT_TITLE: str = "Online Book Store API"
# PROJECT_DESCRIPTION: str = "Backend API for an online Book Store"
CORS_ORIGINS: List[AnyHttpUrl] = []
#settings = Settings()
Inside the list you can add something like "http://localhost:3000"
for a client running on port 3000.
Add list of origins to the app
Inside our main.py
file, we add our CORS config to the API
# from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
# from app.endpoints.api import api_router
# from app.core.config import settings
# app = FastAPI(title=settings.PROJECT_TITLE, description=settings.PROJECT_DESCRIPTION)
# app.include_router(api_router, prefix=settings.API_STR)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# @app.get("/")
# def index():
# return {"message": "Hello World"}
By doing this, now our API knows which origins (urls) are allowed to interact with our data.
Quick Recap
In this post, we defined the remaining endpoints for our books and added new endpoints for authors. We saw how to use Url and query parameters inside our path operation functions and how to take advantage of python types to get some data validation.
We finally added a global configuration for our API and added the allowed origins to allow different client applications to communicate with our API.
Next steps
In the next post, we’re going to start working with Pydantic validation schemas to improve our request and response models. We will also see utilities functions provide by FastAPI to manage exceptions and errors inside our API.