In this blog post we are going to rebuild Django Polls tutorial API using FastAPI.
What is FastAPI?
FastAPI is a web framework for building APIs. As per its official page, `FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
It is easy to learn, fast and said to be high performance, on par with NodeJS
and Go
.`
Installation
Open your terminal and run
pip install fastapi
also need to install ASGI server
pip install uvicorn
thats all, now lets quicky create some endpoints, create a file main.py
and add the following
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def index():
return {"message": "Welcome to the world of FastAPI!"}
@app.get("/items/{item}")
def read_item(item: str, q: str = None):
return {"item": item, "q": q}
now run the server
uvicorn main:app
open your browser and visit http://127.0.0.1:8000/
you should see the following response:
{"message":"Welcome to the world of FastAPI!"}
visit http://127.0.0.1:8000/items/apple?q=delicious
you should see the below response:
{"item":"apple","q":"delicious"}
that’s great, we have already created an API having two endpoints:
http://127.0.0.1:8000/
doesn’t take any parameters and it simply returns a JSON response.http://127.0.0.1:8000/items/{item}"
takes a parameteritem
of typestr
and optionalstr
query parameterq
.
another good feature of FastAPI is that it provides an interactive API documentation, simply visit http://127.0.0.1:8000/docs
or http://127.0.0.1:8000/redoc
.
Now let’s go ahead and rebuild our polls tutorial API. The endpoints we created above are static, they don’t interact with the database. In the next section you will learn how we can use SQLAlchemy for ORM and Pydantic to create models/schemas to make our APIs dynamic.
This post assumes that you’re familiar with SQLAlchemy
, you can refer this docs for more details.
We will create the following endpoints
- An API to create poll question
- API to list all poll questions
- API to get question detail
- API to edit poll question
- API to delete poll question
- API to create choice for a particular poll question
- API to update votes for a particular question
Our project structure would look like this
└───pollsapi
│--- crud.py
│--- database.py
│--- main.py
│--- models.py
│--- schemas.py
Now let’s add the following code to pollsapi/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "postgresql://YOUR_USERNAME:YOUR_PASSWORD@localhost:5432/DATABASE_NAME"
engine = create_engine(
SQLALCHEMY_DATABASE_URL
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
after that, add the following code to pollsapi/models.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
class Question(Base):
__tablename__ = "question"
id = Column(Integer, primary_key=True)
question_text = Column(String(200))
pub_date = Column(DateTime)
choices = relationship('Choice', back_populates="question")
class Choice(Base):
__tablename__ = "choice"
id = Column(Integer, primary_key=True)
question_id = Column(Integer, ForeignKey('question.id', ondelete='CASCADE'))
choice_text = Column(String(200))
votes = Column(Integer, default=0)
question = relationship("Question", back_populates="choices")
we have created relationship
provided by SQLAlchemy ORM, with this we can simply access attribute like question.choices
to get all the choices for that particular question. Similarly the we can refer choice.question
to get question object related to that choice.
Ok, so far so good, we will now create schemas using the pydantic
library.
Go ahead and add the following code to pollsapi/schemas.py
from datetime import datetime
from pydantic import BaseModel
from typing import List
# Choice schema
class ChoiceBase(BaseModel):
choice_text: str
votes: int = 0
class ChoiceCreate(ChoiceBase):
pass
class ChoiceList(ChoiceBase):
id: int
class Config:
orm_mode = True
# Question schema
class QuestionBase(BaseModel):
question_text: str
pub_date: datetime
class QuestionCreate(QuestionBase):
pass
class Question(QuestionBase):
id: int
class Config:
orm_mode = True
class QuestionInfo(Question):
choices: List[ChoiceList] = []
defining attributes in SQLAlchemy is different as compared with Pydantic, in SQLAlchemy arributes are defined using =
and the type is passed as a parameter to Column
like this
question_text = Column(String)
whereas the Pydantic style declares the type using :
like this
question_text: str
Pyndatic models/schemas will be mapped to the incoming data (request data in POST, PUT) and to the response data returned from the API.
We have created base classes QuestionBase
and ChoiceBase
that extends pydantic BaseModel
to hold attributes which are common for creating or reading data and created other classes that inherit from these base classes, the reason being we want specific attributes for creation and reading.
like for example - for creating a choice we need choice_text
and votes
(if not passed, it defaults to 0) so we will use ChoiceCreate
and for reading the choice, we want to return id
, choice_text
and votes
and in this case we will use ChoiceList
.
Another important thing to understand is the use of orm_mode = True
, notice we have added a class Config
and have set orm_mode = True
, this is because by default Pydantic model could read the data from dict
and it can’t read the data if the data is an ORM model so with the orm_mode = True
added to our class, Pydantic model can also read the data from the object something like data.question_text
.
Ok, we will now create pollsapi/crud.py
which will contain all the functions to perform CRUD (Create, Retrieve, Update and Delete) operations.
Add the following code to pollsapi/crud.py
from sqlalchemy.orm import Session
from models import Base, Question, Choice
import schema
# Question
def create_question(db: Session, question: schema.QuestionCreate):
obj = Question(**question.dict())
db.add(obj)
db.commit()
return obj
def get_all_questions(db: Session):
return db.query(Question).all()
def get_question(db:Session, qid):
return db.query(Question).filter(Question.id == qid).first()
def edit_question(db: Session, qid, question: schema.QuestionCreate):
obj = db.query(Question).filter(Question.id == qid).first()
obj.question_text = question.question_text
obj.pub_date = question.pub_date
db.commit()
return obj
def delete_question(db: Session, qid):
db.query(Question).filter(Question.id == qid).delete()
db.commit()
# Choice
def create_choice(db:Session, qid: int, choice: schema.ChoiceCreate):
obj = Choice(**choice.dict(), question_id=qid)
db.add(obj)
db.commit()
return obj
def update_vote(choice_id: int, db:Session):
obj = db.query(Choice).filter(Choice.id == choice_id).first()
obj.votes += 1
db.commit()
return obj
we have created all the utility functions which will be used in API functions.
Now comes the real file pollsapi/main.py
, which will make use of all the files we created above.
Create a poll question
Add the following lines to pollsapi/main.py
from fastapi import FastAPI, HTTPException, Response, Depends
import schema
from typing import List
from sqlalchemy.orm import Session
import crud
from database import SessionLocal, engine
from models import Base
Base.metadata.create_all(bind=engine)
app = FastAPI()
# Dependency
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
## Question
@app.post("/questions/", response_model=schema.QuestionInfo)
def create_question(question: schema.QuestionCreate, db: Session = Depends(get_db)):
return crud.create_question(db=db, question=question)
- This line
Base.metadata.create_all(bind=engine)
creates database tables by using the SQLAlchemy models we defined inpollsapi/models.py
. - function
create_question
is decorated using theapp
object created above which is an instance ofFastAPI
, it takes two argumentspath
andresponse_model
.response_model
returns the schemaQuestionInfo
so the endpoint will return the fieldsid
,question_text
andpub_date
.` - We have created a function
create_question
, first argument receives the request data and maps it to the schemaQuestionCreate
which has the fieldsquestion_text
andpub_date
and the second argument creates a session/request and then it gets closed after the request is completed.
now visit http://127.0.0.1:8000/docs
and you should see section to POST /questions/
something like this
click on that section and it will expand, now click on Try it out
to test your API.
List all poll questions
We will now create an endpoint to get all our poll questions, for this we will use @app.get
, add another function in pollsapi/main.py
@app.get("/questions/", response_model=List[schema.Question])
def get_questions(db: Session = Depends(get_db)):
return crud.get_all_questions(db=db)
notice the use of List
in response_model
, crud.get_all_questions
returns a list of objects and not just an object so we should let our framework know that. Try removing the List
and our application will throw an error.
at this point when you visit http://127.0.0.1:8000/docs
, you should see two sections - POST /questions/
and GET /questions/
, click on GET section and Try it out and you should see a response something like below
[
{
"question_text": "What is fastAPI?",
"pub_date": "2020-05-14T12:58:05.043000",
"id": 1
}
]
Retrieve, Edit and Delete a poll question
def get_question_obj(db, qid):
obj = crud.get_question(db=db, qid=qid)
if obj is None:
raise HTTPException(status_code=404, detail="Question not found")
return obj
@app.get("/questions/{qid}", response_model=schema.QuestionInfo)
def get_question(qid: int, db: Session = Depends(get_db)):
return get_question_obj(db=db, qid=qid)
@app.put("/questions/{qid}", response_model=schema.QuestionInfo)
def edit_question(qid: int, question: schema.QuestionCreate, db: Session = Depends(get_db)):
get_question_obj(db=db, qid=qid)
obj = crud.edit_question(db=db, qid=qid, question=question)
return obj
@app.delete("/questions/{qid}")
def delete_question(qid: int, db: Session = Depends(get_db)):
get_question_obj(db=db, qid=qid)
crud.delete_question(db=db, qid=qid)
return {"detail": "Question deleted", "status_code": 204}
We have used different response_model for get_questions
and get_question
, this is because we wanted to show choices
in the API response only in case of Question detail API and not for Question list API.
API to create choice for a particular poll question
@app.post("/questions/{qid}/choice", response_model=schema.ChoiceList)
def create_choice(qid: int, choice: schema.ChoiceCreate, db: Session = Depends(get_db)):
get_question_obj(db=db, qid=qid)
return crud.create_choice(db=db, qid=qid, choice=choice)
and finally
API to update votes for a particular question
@app.put("/choices/{choice_id}/vote", response_model=schema.ChoiceList)
def update_vote(choice_id: int, db: Session = Depends(get_db)):
return crud.update_vote(choice_id=choice_id, db=db)
the following are the endpoints for Question and Choice
- Create question -
POST http://127.0.0.1:8000/questions/
- List all questions -
GET http://127.0.0.1:8000/questions/
- Retrieve a particular question -
GET http://127.0.0.1:8000/questions/{qid}
- Edit a particular question -
PUT http://127.0.0.1:8000/questions/{qid}
- Delete a particular question -
DELETE http://127.0.0.1:8000/questions/{qid}
- Create choice for a particular poll question -
POST http://127.0.0.1:8000/questions/{qid}/choice
- Update votes for a particular question -
PUT http://127.0.0.1:8000/choices/{choice_id}/vote
You can find a source code here
Thank you for reading the Agiliq blog. This article was written by Manjunath Hugar on May 14, 2020 in FastAPI , Python , SQLAlchemy , Pydantic .
You can subscribe ⚛ to our blog.
We love building amazing apps for web and mobile for our clients. If you are looking for development help, contact us today ✉.
Would you like to download 10+ free Django and Python books? Get them here