Create REST API with FastAPI and SQLAlchemy

- Set up project structure with FastAPI
- Create SQLite database with SQLAlchemy
- Implement Alembic for database migrations
- Add CRUD operations for items resource
- Create health endpoint
- Update documentation

generated with BackendIM... (backend.im)
This commit is contained in:
Automated Action 2025-05-14 10:26:10 +00:00
parent 673a64e8a1
commit a4ffe78f61
18 changed files with 453 additions and 2 deletions

View File

@ -1,3 +1,71 @@
# FastAPI Application
# Generic REST API
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A RESTful API built with FastAPI and SQLAlchemy.
## Features
- FastAPI for high-performance API with automatic OpenAPI documentation
- SQLAlchemy ORM for database operations
- SQLite database for storage
- Alembic for database migrations
- Pydantic models for request/response validation
- CRUD operations for the 'items' resource
- Health check endpoint
## Project Structure
```
├── alembic/ # Database migration scripts
├── app/ # Application source code
│ ├── api/ # API endpoints
│ │ ├── endpoints/ # API routes
│ ├── crud/ # CRUD operations
│ ├── db/ # Database configurations
│ ├── models/ # SQLAlchemy models
│ └── schemas/ # Pydantic schemas
├── storage/ # Storage directory
│ └── db/ # Database files
├── alembic.ini # Alembic configuration
├── main.py # Application entry point
└── requirements.txt # Project dependencies
```
## Installation and Setup
1. Clone the repository
2. Install the dependencies:
```bash
pip install -r requirements.txt
```
3. Apply the database migrations:
```bash
alembic upgrade head
```
4. Run the application:
```bash
uvicorn main:app --reload
```
## API Documentation
Once the application is running, you can access the API documentation at:
- Swagger UI: [http://localhost:8000/docs](http://localhost:8000/docs)
- ReDoc: [http://localhost:8000/redoc](http://localhost:8000/redoc)
## API Endpoints
### Items Resource
- `GET /items`: Get all items (paginated)
- `POST /items`: Create a new item
- `GET /items/{item_id}`: Get a specific item by ID
- `PUT /items/{item_id}`: Update an item
- `DELETE /items/{item_id}`: Delete an item
### Health Check
- `GET /health`: Check API health status

41
alembic.ini Normal file
View File

@ -0,0 +1,41 @@
[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
# SQLite URL example
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration for SQLAlchemy.

80
alembic/env.py Normal file
View File

@ -0,0 +1,80 @@
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
from app.db.base import Base # noqa
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,36 @@
"""create items table
Revision ID: 001
Revises:
Create Date: 2025-05-14
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'item',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_item_id'), 'item', ['id'], unique=False)
op.create_index(op.f('ix_item_title'), 'item', ['title'], unique=False)
def downgrade():
op.drop_index(op.f('ix_item_title'), table_name='item')
op.drop_index(op.f('ix_item_id'), table_name='item')
op.drop_table('item')

0
app/api/__init__.py Normal file
View File

View File

@ -0,0 +1,5 @@
from fastapi import APIRouter
from app.api.endpoints import items
router = APIRouter()
router.include_router(items.router, prefix="/items", tags=["items"])

View File

@ -0,0 +1,53 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.crud import item as crud
from app.schemas.item import Item, ItemCreate, ItemUpdate
from app.db.session import get_db
router = APIRouter()
@router.get("/", response_model=List[Item])
def read_items(
skip: int = 0, limit: int = 100, db: Session = Depends(get_db)
):
items = crud.get_items(db, skip=skip, limit=limit)
return items
@router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED)
def create_item(
item: ItemCreate, db: Session = Depends(get_db)
):
return crud.create_item(db=db, item=item)
@router.get("/{item_id}", response_model=Item)
def read_item(
item_id: int, db: Session = Depends(get_db)
):
db_item = crud.get_item(db, item_id=item_id)
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
return db_item
@router.put("/{item_id}", response_model=Item)
def update_item(
item_id: int, item: ItemUpdate, db: Session = Depends(get_db)
):
db_item = crud.update_item(db, item_id=item_id, item=item)
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
return db_item
@router.delete("/{item_id}", response_model=Item)
def delete_item(
item_id: int, db: Session = Depends(get_db)
):
db_item = crud.delete_item(db, item_id=item_id)
if db_item is None:
raise HTTPException(status_code=404, detail="Item not found")
return db_item

8
app/api/health.py Normal file
View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/health", tags=["health"])
def health_check():
return {"status": "ok", "message": "API is running"}

39
app/crud/item.py Normal file
View File

@ -0,0 +1,39 @@
from sqlalchemy.orm import Session
from typing import List, Optional
from app.models.item import Item
from app.schemas.item import ItemCreate, ItemUpdate
def get_item(db: Session, item_id: int) -> Optional[Item]:
return db.query(Item).filter(Item.id == item_id).first()
def get_items(db: Session, skip: int = 0, limit: int = 100) -> List[Item]:
return db.query(Item).offset(skip).limit(limit).all()
def create_item(db: Session, item: ItemCreate) -> Item:
db_item = Item(**item.dict())
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
def update_item(db: Session, item_id: int, item: ItemUpdate) -> Optional[Item]:
db_item = get_item(db, item_id)
if db_item:
update_data = item.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_item, key, value)
db.commit()
db.refresh(db_item)
return db_item
def delete_item(db: Session, item_id: int) -> Optional[Item]:
db_item = get_item(db, item_id)
if db_item:
db.delete(db_item)
db.commit()
return db_item

3
app/db/base.py Normal file
View File

@ -0,0 +1,3 @@
# Import all the models, so that alembic can detect them
from app.db.base_class import Base
from app.models.item import Item

12
app/db/base_class.py Normal file
View File

@ -0,0 +1,12 @@
from typing import Any
from sqlalchemy.ext.declarative import as_declarative, declared_attr
@as_declarative()
class Base:
id: Any
__name__: str
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

23
app/db/session.py Normal file
View File

@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from pathlib import Path
# Create db directory if it doesn't exist
DB_DIR = Path("/app") / "storage" / "db"
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

11
app/models/item.py Normal file
View File

@ -0,0 +1,11 @@
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from app.db.base_class import Base
class Item(Base):
id = Column(Integer, primary_key=True, index=True)
title = Column(String(100), index=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

25
app/schemas/item.py Normal file
View File

@ -0,0 +1,25 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class ItemBase(BaseModel):
title: str
description: Optional[str] = None
class ItemCreate(ItemBase):
pass
class ItemUpdate(ItemBase):
title: Optional[str] = None
class Item(ItemBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True

16
main.py Normal file
View File

@ -0,0 +1,16 @@
from fastapi import FastAPI
from app.api.endpoints import router as api_router
from app.api.health import router as health_router
app = FastAPI(
title="Generic REST API",
description="A generic REST API built with FastAPI and SQLAlchemy",
version="0.1.0",
)
app.include_router(api_router)
app.include_router(health_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
fastapi==0.103.1
uvicorn==0.23.2
sqlalchemy==2.0.21
pydantic==2.4.2
alembic==1.12.0
ruff==0.0.292