Introduction to FastAPI

Receiving and returning data with FastAPI

Resources

In this course, we'll build a social media API. Users will be able to publish posts and write comments on them.

Let's start with publishing posts:

  • Everyone will be able to post posts, without signing up first.
  • Everyone will be able to see everyone's posts.

Doing this makes it much simpler. We don't have to worry about user authentication or the concept of "followers". We'll add those in later!

Creating an endpoint to receive a user's post

Imagine we are backend developers in a social media company, and we're tasked with making this API. There's another team that is tasked with making the website, and a third team that makes the mobile app. Both the website and mobile app will use your API, and that ensures that they both work in the same way.

APIs work by allowing clients (in our case, there's two) to send it data. The API then can do some processing, such as storing it in the database or sending an email, and then it responds to the client with some other data.

The first step in allowing users to publish posts is to add an API endpoint to which their client can send data. It doesn't matter to us if the client is the website or the mobile app, both will send us the same data. We'll then store that data in our database and respond with a success message.

First let's define what data clients will need to send us for their posts. Let's begin with something simple: the user's name and their post. Both of these are just text.

Where does the client get the user's name from?

As the API developer, we really don't care! The website or app could ask the user for their name when the user writes the post, or when they hit 'Publish', or at any other time. It's simply not something we need to worry about.

FastAPI relies strongly on another library called pydantic to model the different bits of data a user might send. To create the model, create a file called models/post.py with this content:

models/post.py
from pydantic import BaseModel

class UserPost(BaseModel):
    name: str
    body: str

Now let's import that model in our main.py file:

main.py
from fastapi import FastAPI
from models.post import UserPost

app = FastAPI()

And then let's create an endpoint to which users can send JSON. We'll then give the JSON to our model, and it will spit back out a Python object with two attributes: name and body.

The endpoint will use an HTTP POST method. The HTTP method is something that every request to our API will have. It's just a piece of data in the request that helps tell the API what the request is for.

What are HTTP methods?

The HTTP method is just some data in the request. For example, if the method is GET, that usually means the user wants our API to give them some data. If the method is POST, that usually means the user wants to send our API some data (to tell our API to create something, or store something in the database).

There are also other HTTP methods, such as PUT, PATCH, DELETE, and others.

Important: the HTTP methods themselves don't have much meaning. It's up to our API to decide what to do with each request, and the HTTP method gives us a bit more context about what the user wants.

Since the user wants us to create something in our API, let's use the POST HTTP method for our endpoint:

main.py
from fastapi import FastAPI
from models.post import UserPost

app = FastAPI()

@app.post("/post")
async def create_post(post: UserPost):
    return post

Something very cool about FastAPI is how it uses Python type hinting to simplify the definition of our API. Here by telling it that the post parameter is of type UserPost, it will automatically pass the incoming request's JSON body through our UserPost model. Then a few things happen:

  • The UserPost model validates the incoming JSON. It makes sure the name and body fields are in there, and they can be converted to strings if they aren't already.
  • Converts the JSON into a UserPost object and passes that to our create_post function, so that post is a Python object with two attributes: name and body. We don't have to parse the JSON ourselves.
  • Helps populate the autogenerated documentation with information about what the client should send the API.

We've made the endpoint return the newly created post, but we could just return a message saying "Post created successfully" or something along those lines. In any case, the client should understand that receiving the newly created post JSON means "success".

Storing and returning user posts

Now let's store the post in a list so that future API requests can read them:

main.py
from fastapi import FastAPI
from models.post import UserPost

app = FastAPI()
posts = []

@app.post("/post")
async def create_post(post: UserPost):
    posts.append(post)
    return post

All we've done here is added the posts global variable, and then after we receive the client's data, we add the post to the list.

Finally, let's make another endpoint that returns all the posts for everyone to see:

main.py
from fastapi import FastAPI
from models.post import UserPost

app = FastAPI()
posts = []

@app.post("/post", response_model=UserPost)
async def create_post(post: UserPost):
    posts.append(post)
    return post

@app.get("/posts", response_model=list[UserPost])
async def get_all_posts():
    return posts

Here we've addedresponse_model=UserPost and response_model=list[UserPost] to the decorators, beside the endpoint URL itself. This tells FastAPI how to convert our response to JSON.

The create_post endpoint will pass the post object to UserPost, and be turned to a dictionary. FastAPI then returns the dictionary as JSON text.

The get_all_posts endpoint will expect a list of UserPost objects. Each object will be turned into a dictionary by the pydantic library, and then the list will be returned as JSON text.

HTTP status codes

Just as the HTTP method is given to us by the client of our API and gives us a bit more information about what they want to achieve, we can return some data that tells the client the status of their request.

I'm sure you already know of some infamous status codes like 404 or 500. Each status code means something specific, such as "Not Found" or "Internal Server Error".

The default status code, which means "OK", is 200. That's what FastAPI will use for all responses unless we tell it to do something else.

When we create something we should also return a specific status code: 201. Let's use that when we create posts:

main.py
from fastapi import FastAPI
from models.post import UserPost

app = FastAPI()
posts = []

@app.post("/post", status_code=201, response_model=UserPost)
async def create_post(post: UserPost):
    posts.append(post)
    return post