In this tutorial, you'll learn how to develop a CRUD API with FastAPI and deploy the application to Okteto Cloud.
You'll start by building the code of the application, then we'll define the application with Okteto Stacks, and finally, you will deploy it to Okteto Cloud.
What is FastAPI
FastAPI is a modern Python web framework designed for building fast and efficient backend applications. It comes with built-in support for data validation, authentication, and interactive API documentation powered by OpenAPI and Swagger.
What is Okteto?
Okteto is a developer platform used to accelerate the development workflow of cloud-native applications. We'll use Okteto to run our application once we are done with the code.
Initial Setup
Start by creating a new folder to hold your project called 'fastapi-crud':
$ mkdir fastapi-crud
$ cd fastapi-crud
Next, create and activate a virtual environment in the project folder:
$ python3.9 -m venv venv
$ source venv/bin/activate
$ export PYTHONPATH=$PWD
Virtual environment provides an isolated environment to run our Python applications. FastAPI applications require an isolated environment to manage its dependencies such as Uvicorn which is an Asynchronous GateWay Server Interface server.
Next, create the following files and folder:
├── app
│ ├── __init__.py
│ ├── model.py
│ ├── api.py
├──main.py
└── requirements.txt
In your requirements.txt file, add the following dependencies:
fastapi==0.62.0
uvicorn==0.13.1
The first dependency, fastapi,
is the framework on which your application will be built. The second dependency, uivorn
, is an Asynchronous Gateway Server Interface (ASGI) that enables us to run our FastAPI application.
Install the dependencies:
(venv)$ pip install -r requirements.txt
With the installation complete, define a base route in app/api.py
:
from fastapi import FastAPI
app = FastAPI()
@app.get("/", tags=["Home"])
def get_root() -> dict:
returrn {
"message": "Welcome to the okteto's app."
}
You started by importing the FastAPI class from the fastapi
package in the code block above. Then you created an instance of the class in the variable app
.
Next, you defined a GET
route on "/" which is handled by the get_root()
function.
In the main.py
file, define an entry point for running the application:
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.api:app", host="0.0.0.0", port=8080, reload=True)
In the code block above, you imported the uvicorn package itself. Under the initializer block, you invoked the run
method, which takes the location of FastAPI's instance, the host, port, and the reload boolean value.
In this application, the location of the FastAPI instance, app = FastAPI()
is in the file app/api.py
. The host value can be set to a valid IP address within your local machine's configured IP scope, it can also be left blank. Likewise the port and reload value. The port 8080
is chosen since Okteto applications run on port 8080 by default, the reload value is set to True to avoid restarting the application on every change made.
Next, start the application:
$ python main.py
You should get a response like this:
INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
INFO: Started reloader process [24513] using statreload
INFO: Started server process [24515]
INFO: Waiting for application startup.
INFO: Application startup complete.
Navigate to http://localhost:8080 in your browser. You should see:
{
"message": "Welcome to the okteto's app."
}
Routes
You'll be building a recipe application where you can store, remove, update and delete recipes by sending HTTP requests.
Before you begin writing the routes, define the model schema for the application:
In app/model.py
, write the following:
from pydantic import BaseModel, Field
from typing import Optional, List
class RecipeSchema(BaseModel):
id: Optional[int]
name: str = Field(...)
ingredients: List[str] = Field(...)
class Config:
schema_extra = {
"example": {
"name": "Donuts",
"ingredients": ["Flour", "Milk", "Sugar", "Vegetable Oil"]
}
}
class UpdateRecipeSchema(BaseModel):
name: Optional[str]
ingredients: Optional[List[str]]
class Config:
schema_extra = {
"example": {
"name": "Buns",
"ingredients": ["Flour", "Milk", "Sugar", "Vegetable Oil"]
}
}
In the block of code above, you defined how the recipe would be represented, sent, and retrieved in your in-app database. In a realistic scenario, you should use a separate database like MySQL, Postgres or MongoDB.
Each recipe will have an ID automatically generated once a POST request is sent. Each recipe will also comprise a name and an array of ingredients. If any other type of data not listed in the schema is sent in the request body, an error will be returned.
The RecipeSchema
has a subclass Config
. The subclass contains an object variable, schema_extra
, which includes a key example
used as the mock data in the interactive documentation.
The UpdateRecipeSchema
, on the other hand, is the model for guiding request body sent through the UPDATE route. The difference between this model and the RecipeSchema
is that the schema values can be passed optionally.
With the model in place, let's write the code for the application's routes. In app/api.py
, add the recipes database before the base route "/":
recipes = [
{
"id": 1,
"name": "Donuts",
"ingredients": ["Flour", "Milk", "Sugar", "Vegetable Oil"]
}
]
Below the base route, add the GET routes:
@app.get("/recipe", tags=["Recipe"])
def get_recipes() -> dict:
return {
"data": recipes
}
@app.get("/recipe/{id}", tags=["Recipe"])
def get_recipe(id: int) -> dict:
if id > len(recipes) or id < 1:
return {
"error": "Invalid ID passed."
}
for recipe in recipes:
if recipe['id'] == id:
return {
"data": [
recipe
]
}
return {
"error": "No such recipe with ID {} exist".format(id)
}
You defined GET routes for retrieving all the recipes and a single recipe using its ID in the code block above. An error message is returned if an invalid ID is passed as a parameter in the /recipe/{id} route. Test the routes by visiting http://localhost:8080/recipe:
And http://localhost:8080/recipe/1:
Define the POST route for adding new recipes. Start by updating the imports:
from fastapi import FastAPI, Body
from app.model import RecipeSchema, UpdateRecipeSchema
from fastapi.encoders import jsonable_encoder
Next, add the POST route beneath the GET routes:
@app.post("/recipe", tags=["Recipe"])
def add_recipe(recipe: RecipeSchema = Body(...)) -> dict:
recipe.id = len(recipes) + 1
recipes.append(recipe.dict())
return {
"message": "Recipe added successfully."
}
In the code block above, we made sure the request body is modeled in our RecipeSchema
by setting the type recipe
to RecipeSchema
in the add_recipe
function. The Body(...)
imported from FastAPI ensures that the request body is passed.
In an event where the content of the request doesn't match the specified schema type, an error message automatically generated from Pydantic will be generated and then returned.
In the add_recipe
function, the recipe ID is computed by incrementing the length of the recipes database by 1. Test the POST route using curl:
$ curl -X POST http://localhost:8080/recipe -d \
'{"name": "Donut", "ingredients": ["Flour", "Milk", "Butter"]}' \
-H 'Content-Type: application/json'
You will get a response:
{
"message": "Recipe added successfully."
}
To verify that the recipe has been added, retrieve all the recipes:
$ curl -X GET http://localhost:8080/recipe/2 -H 'Content-Type: application/json'
You will get this response:
{
"data": [
{"id":1,"name":"Donuts","ingredients":["Flour","Milk","Sugar","Vegetable Oil"]},
{"id":2,"name":"Donut","ingredients":["Flour","Milk","Butter"]},
]
}
You can also retrieve the recipe from its ID:
$ curl -X GET http://localhost:8080/recipe/2 -H 'Content-Type: application/json'
You will get the response:
{
"data": [
{"id":2,"name":"Donut","ingredients":["Flour","Milk","Butter"]}
]
}
With the POST route in place, define the UPDATE route:
@app.put("/recipe", tags=["Recipe"])
def update_recipe(id: int, recipe_data: UpdateRecipeSchema) -> dict:
stored_recipe = {}
for recipe in recipes:
if recipe["id"] == id:
stored_recipe = recipe
if not stored_recipe:
return {
"error": "No such recipe exists."
}
stored_recipe_model = RecipeSchema(**stored_recipe)
update_recipe = recipe_data.dict(exclude_unset=True)
updated_recipe = stored_recipe_model.copy(update=update_recipe)
recipes[recipes.index(stored_recipe_model)] = jsonable_encoder(updated_recipe)
return {
"message": "Recipe updated successfully."
}
In the code block above, you find a recipe by the ID supplied and perform a partial or full update depending on the content of the request body passed. If no such ID exists, it returns an error message.
Test the UPDATE route by changing the title of the first recipe to Buns using curl:
$ curl -X PUT "http://0.0.0.0:8080/recipe?id=1" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"name\": \"Buns\",}"
The UPDATE request returns this response:
{
"message": "Recipe updated successfully."
}
Let's define the final route, DELETE. The DELETE route is used to remove recipe in your application using their ID. Beneath the UPDATE route, add the following:
@app.delete("/recipe/{id}", tags=["Recipe"])
def delete_recipe(id: int) -> dict:
if id > len(recipes) or id < 1:
return {
"error": "Invalid ID passed"
}
for recipe in recipes:
if recipe['id'] == id:
recipes.remove(recipe)
return {
"message": "Recipe deleted successfully."
}
return {
"error": "No such recipe with ID {} exist".format(id)
}
In the code block above, you first check if the recipe whose ID is passed exists. If it does, the recipe is removed. Otherwise, an error message is returned. Test the DELETE route:
$ curl -X DELETE "http://0.0.0.0:8080/recipe/1" -H "accept: application/json"
The request above return this response:
{
"message": "Recipe deleted successfully."
}
You have successfully built the CRUD application.
Deploying to Okteto Cloud
In this section, you will be deploying the application to Okteto Cloud.
Why Deployment?
When you are developing locally, your application is inaccessible by other people since it runs only on your machine's localhost. Deploying it on a cloud service like Okteto enables you and your collaborators to access it through its unique URL address, just like your customers. On top of that, any application deployed in Okteto Cloud automatically gets a free HTTPs endpoint.
Create a free developer account on Okteto if you do not have one setup. Additionally, install the Okteto CLI on your machine.
Before proceeding, create a .gitignore file in the project folder to prevent checking in the "venv" folder to git:
$ touch .gitignore
Add the following:
venv
__pycache__
Next, log into your docker hub account from the console:
$ docker login
If you do not have a docker hub account, visit the DockerHub Website to create one. A docker hub account is necessary to store images built from running the deploy command.
With the necessary installations in place, log on to Okteto from your terminal using the command:
$ okteto login
The next step is to create a stack manifest file for Okteto. The manifest file removes the need to deal with the complexities of Kubernetes manifests.
$ touch okteto-stack.yaml
In the okteto-stack.yaml
file, define the application using a format that's very similar to docker-compose:
name: fastapi-crud
services:
fastapi:
public: true
image: DOCKERHUBUSERNAME/fastapi-crud:latest
build: .
replicas: 1
ports:
- 8080
resources:
cpu: 100m
memory: 128Mi
In the manifest file above, you defined your application's name and the services your app is made up of, fastapi. The fastapi service exposes your application via the port 8080
to a public HTTPs endpoint with a valid certificate. The service also allocates itself with 100 mili CPUs and 128Mi of memory. Replace DOCKERHUBUSERNAME in the file above with your docker hub username.
You can learn more about the Okteto Stacks manifest format here.
With the manifest in place, create a Dockerfile to house the build instruction for the application. Dockerfile contains build instruction for images to be deployed on Okteto.
$ touch Dockerfile
Add the following:
FROM python:3.8
ADD requirements.txt /requirements.txt
ADD main.py /main.py
ADD okteto-stack.yaml /okteto-stack.yaml
RUN pip install -r requirements.txt
EXPOSE 8080
COPY ./app app
CMD ["python3", "main.py"]
Let's deploy your application to Okteto. Start by running the command:
$ okteto namespace
The command above setups a namespace on the Okteto cloud. Next, run the command to deploy the application:
$ okteto stack deploy --build
The command above builds the service you indicated in the okteto-stack.yml
file and then deploys the application. This command eases the stress of having to build, then manually configuring the application after deployment.
The command above starts the application. Navigate to your dashboard and click the link under the "Endpoints" heading:
Go on and test the endpoints.
Conclusion
In this article, you built a CRUD recipe application and deployed it to Okteto.
You witnessed the simplicity and ease of deployment of an application using Okteto and how fast and simple it is to build FastAPI applications.
The application can also be deployed directly to Okteto from a GitHub repository by following this Step By Step Guide.
The code used in this article can be found in GitHub.
Top comments (0)