In this tutorial we learn to create REST Apis with fastapi
. We are going to use:
FastApi
for creating APIsPostgreSQL
as a databaseSQLAlchemy
as an ORM toolAlembic
as a migration tool
Tech and tools used
- Linux os (you can use window or mac also. But with windows your commands might be a little different. I suggest you to use
GitBash
in windows) - I am using
uv
to create project, you can download it from here - Python 3.12
- VS Code (code editor) with extension
Python
by microsoft - PostgreSQL (I am using it in a docker container, you can use installed version too)
The reason I am using uv
, because it is pretty fast and I can easily manage dependencies with it.
Source code
Github repo. If you like my work, please consider starring the repo.
Create the new project
- Create a project with uv
uv init book_crud
- Execute the command
code book_crud
. It will open this project in VS Code
Open the integrated terminal of vs code
and fire these commands:
uv venv
(will create a virtual environment)source .venv/bin/activate
(will activate venv)uv add Fastapi SQLAlchemy alembic psycopg2-binary python-dotenv uvicorn
- This command will add all ther required libraries
SQLAlchemy
is an ORMalembic
is a migration toolpsycopg2-binary
is a PostgreSQL database adapterpython-dotenv
is used for reading.env
fileuvicorn
is a server
touch .env
mkdir src
cd src
mkdir models schemas database services routers
touch main.py
for d in models schemas database services routers; do touch $d/__init__.py; done
. This will create__init__.py
file inside each folder (models,schemas,database,services and routers)
Create a database
Create a database named book_db
in postgres.
Edit the .env file
DEBUG=True
DATABASE_URL=postgresql://postgres:p%4055w0rd@localhost:5432/book_db
Note: my password is p@55w0rd
. p%4055w0rd
is the encoded version of it. I need to provide an encoded password, because it won’t accept @
.
database/database.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase,sessionmaker
from dotenv import load_dotenv
class Base(DeclarativeBase):
pass
load_dotenv()
DATABASE_URL = os.getenv('DATABASE_URL').strip()
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False,autoflush=False,bind=engine)
def get_session():
with SessionLocal() as session:
yield session
# just for testing the db connection
# if __name__ == "__main__":
# conn = engine.connect()
# print("Connected to db")
# conn.close()
models/book.py
from src.database.database import Base
from sqlalchemy import String
from sqlalchemy.orm import Mapped,mapped_column
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(50))
author: Mapped[str] = mapped_column(String(50))
It is a data model which corresponds to a database table.
schemas/book_schemas.py:
These are pydantic schemas used as request and response models.
from pydantic import BaseModel, ConfigDict,Field
class BookCreate(BaseModel):
title:str = Field(min_length=1,max_length=50)
author:str = Field(min_length=1,max_length=50)
class BookResponse(BaseModel):
model_config = ConfigDict(from_attributes=True) # allows Pydantic to read data from ORM model attributes directly!
id:int
title:str
author:str
services/book_services.py
This file contains CRUD operations for book.
from typing import List, Optional
from sqlalchemy.orm import Session
from src.models.book import Book
from src.schemas.book_schemas import BookCreate,BookResponse
def add_book(session:Session, book_create:BookCreate)->BookResponse:
book = Book(**book_create.model_dump())
session.add(book)
session.commit()
session.refresh(book)
book_response = BookResponse.model_validate(book)
return book_response
def update_book(session:Session,book_id:int,book_update:BookCreate)->Optional[BookResponse]:
stmt = session.query(Book).where(Book.id==book_id)
existing_book = session.scalars(stmt).one_or_none()
if existing_book is None:
return None
for key,value in book_update.model_dump().items():
setattr(existing_book,key,value)
session.commit()
session.refresh(existing_book)
return existing_book
def delete_book(session:Session,book_id:int) -> Optional[BookResponse]:
stmt = session.query(Book).where(Book.id==book_id)
existing_book = session.scalars(stmt).one_or_none()
if existing_book is None:
return None
session.delete(existing_book)
session.commit()
return BookResponse.model_validate(existing_book)
def get_book(session:Session,book_id:int)-> Optional[BookResponse]:
stmt = session.query(Book).where(Book.id==book_id)
book = session.scalars(stmt).one_or_none()
if book is None:
return None
book_response = BookResponse.model_validate(book)
return book_response
def get_books(session:Session) -> List[BookResponse]:
stmt = session.queryy(Book)
books = session.scalars(stmt).all()
return [BookResponse.model_validate(book) for book in books]
routers/book_routes.py
from typing import List
from src.services import book_service
from src.schemas.book_schemas import BookCreate, BookResponse
from src.database.database import get_session
from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException
book_router = APIRouter(
prefix="/api/books",
tags=["books"]
)
@book_router.get("/", response_model=List[BookResponse])
def get_books(session: Session = Depends(get_session)):
return book_service.get_books(session)
@book_router.get("/{book_id}", response_model=BookResponse)
def get_book(book_id: int, session: Session = Depends(get_session)):
book = book_service.get_book(session, book_id)
if book is None:
raise HTTPException(status_code=404, detail="Book not found")
return book
@book_router.post("/", response_model=BookResponse, status_code=201)
def add_book(book: BookCreate, session: Session = Depends(get_session)):
return book_service.add_book(session, book)
@book_router.put("/{book_id}", response_model=BookResponse)
def update_book(book_id: int, book: BookCreate, session: Session = Depends(get_session)):
updated_book = book_service.update_book(session, book_id, book)
if updated_book is None:
raise HTTPException(status_code=404, detail="Book not found")
return updated_book
@book_router.delete("/{book_id}", status_code=204)
def delete_book(book_id: int, session: Session = Depends(get_session)):
deleted_book = book_service.delete_book(session, book_id)
if deleted_book is None:
raise HTTPException(status_code=404, detail="Book not found")
main.py
import os
from fastapi import FastAPI
from src.routers.book_routes import book_router
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(debug=os.getenv("DEBUG", "False").lower() == "true")
app.include_router(book_router)
Initialize migration
With migration commands, we create/update the database tables or other things related to the database.
Make sure, you are at the root
directory not in the src
directory.
alembic init alembic
This command with generate alembic
directory and alembic.ini
file in root directory.
Update alembic.ini
Comment out this line sqlalchemy.url = driver://user:pass@localhost/dbname
Edit alembic/env.py
Add these line to env.py
file leave the other content as it is.
## content before this is removed for the sake of brevity
config = context.config
from src.database.database import Base,DATABASE_URL
from src.models.book import Book
# import other models too
url_str = DATABASE_URL.replace("%","%%")
config.set_main_option('sqlalchemy.url',url_str)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
## content after this is removed for the sake of brevity
Note: My password is p@55w0rd
, which is encoded as p%4055w0rd
. That’s’ why I need to replace %
with %%
.
Create and apply migration
alembic revision --autogenerate -m "Inital migration"
It will create a migration file under alembic/version
with a name like f818e6752f3c_inital_migration.py
. Notice that, table is not created in the database yet. For that you have to apply this migration.
You need to review the file first. Make sure it has the content you are expecting. Then fire the command below which will persist changes to the database.
alembic upgrade head
Running the app
uvicorn src.main:app --reload
App is listening at http://127.0.0.1:8000
. You can http://127.0.0.1:8000/docs
and it will open swagger
documentation. You can test your apis there.
I am using a VS Code extension
called REST Client
by Huachao Mao
. If you have installed it, you can create books.http
file and paste the content below:
@base_url = http://localhost:8000/api/books
POST {{base_url}}
Content-Type: application/json
{
"title": "aaa",
"author": "bbb"
}
###
PUT {{base_url}}/8
Content-Type: application/json
{
"title": "jjj",
"author": "aaa"
}
###
GET {{base_url}}
###
GET {{base_url}}/8
###
DELETE {{base_url}}/7
You will notice a link Send Request
above each http verb (GET,PUT,POST,DELETE). You need to press that button.