Создание REST API с помощью FastAPI и SQLite

Раздел: Практика -> Разработка проектов

Основной подход: создание веб-API на FastAPI

Для разработки современного REST API на Python часто выбирают фреймворк FastAPI. Он поддерживает асинхронность, автоматическую документацию и валидацию данных. Рассмотрим создание простого API для управления задачами (TODO).

Шаг 1. Установка зависимостей

Необходимо установить FastAPI, сервер Uvicorn и SQLAlchemy для работы с базой данных:

pip install fastapi uvicorn sqlalchemy

Python пример разработки (пример разработки на python)

Шаг 2. Создание структуры проекта

Создадим файл main.py со следующим содержимым:


from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Настройка базы данных SQLite
SQLALCHEMY_DATABASE_URL = 'sqlite:///./todos.db'
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={'check_same_thread': False})
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
Base = declarative_base()

# Определение модели Task
class Task(Base):
    __tablename__ = 'tasks'
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    completed = Column(Boolean, default=False)

Base.metadata.create_all(bind=engine)

# Pydantic модель для запросов и ответов
class TaskCreate(BaseModel):
    title: str
    completed: bool = False

class TaskResponse(BaseModel):
    id: int
    title: str
    completed: bool

app = FastAPI()

# Зависимость для получения сессии БД
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.post('/tasks/', response_model=TaskResponse)
def create_task(task: TaskCreate):
    db = next(get_db())
    db_task = Task(title=task.title, completed=task.completed)
    db.add(db_task)
    db.commit()
    db.refresh(db_task)
    return db_task

@app.get('/tasks/', response_model=list[TaskResponse])
def read_tasks():
    db = next(get_db())
    tasks = db.query(Task).all()
    return tasks

@app.get('/tasks/{task_id}', response_model=TaskResponse)
def read_task(task_id: int):
    db = next(get_db())
    task = db.query(Task).filter(Task.id == task_id).first()
    if not task:
        raise HTTPException(status_code=404, detail='Task not found')
    return task

@app.put('/tasks/{task_id}', response_model=TaskResponse)
def update_task(task_id: int, task: TaskCreate):
    db = next(get_db())
    db_task = db.query(Task).filter(Task.id == task_id).first()
    if not db_task:
        raise HTTPException(status_code=404, detail='Task not found')
    db_task.title = task.title
    db_task.completed = task.completed
    db.commit()
    db.refresh(db_task)
    return db_task

@app.delete('/tasks/{task_id}')
def delete_task(task_id: int):
    db = next(get_db())
    db_task = db.query(Task).filter(Task.id == task_id).first()
    if not db_task:
        raise HTTPException(status_code=404, detail='Task not found')
    db.delete(db_task)
    db.commit()
    return {'ok': True}
  

Шаг 3. Запуск сервера

Выполните команду в терминале:

uvicorn main:app --reload

Сервер запустится на http://127.0.0.1:8000. Документация доступна по адресу /docs.

Типичные ошибки и их решения:

  • Ошибка импорта sqlalchemy - не установлен пакет. Решение: выполнить pip install sqlalchemy.
  • Ошибка No module named 'pydantic' - требуется установка pydantic, обычно входит в состав FastAPI, но если нет, установить отдельно.
  • Проблемы с потоками SQLite: в строке подключения указан параметр check_same_thread=False. Без него FastAPI может выдавать ошибку при работе с несколькими запросами.
  • Ошибка 404 при обращении к несуществующему id - код предусматривает обработку, но если забыть, будет исключение.

Как создать аналогичное API с помощью Flask?

Flask - более простой фреймворк. Для REST API используют расширение Flask-RESTful или просто декораторы. Пример:


from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todos.db'
db = SQLAlchemy(app)

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100))
    completed = db.Column(db.Boolean, default=False)

with app.app_context():
    db.create_all()

@app.route('/tasks', methods=['POST'])
def create_task():
    data = request.get_json()
    task = Task(title=data['title'], completed=data.get('completed', False))
    db.session.add(task)
    db.session.commit()
    return jsonify({'id': task.id, 'title': task.title, 'completed': task.completed}), 201

@app.route('/tasks', methods=['GET'])
def get_tasks():
    tasks = Task.query.all()
    return jsonify([{'id': t.id, 'title': t.title, 'completed': t.completed} for t in tasks])

# ... другие маршруты
  

Проблемы Flask подхода:

  • Необходимость ручной валидации данных (отсутствие Pydantic).
  • Меньшая производительность по сравнению с FastAPI.
  • Отсутствие автоматической документации.

Какие возможности даёт Django REST Framework?

Для крупных проектов выбирают Django с DRF. Пример сериализатора и вьюсета:


# serializers.py
from rest_framework import serializers
from .models import Task

class TaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = '__all__'

# views.py
from rest_framework import viewsets
from .models import Task
from .serializers import TaskSerializer

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
  

Особенности и сложности:

  • Большой объем кода и конфигурации.
  • Высокий порог входа.
  • Мощная админка и ORM.

Как обойтись без ORM, используя чистый SQL?

Для простых проектов можно напрямую работать с sqlite3. Пример:


import sqlite3
from fastapi import FastAPI, HTTPException

app = FastAPI()

def get_db_connection():
    conn = sqlite3.connect('todos.db')
    conn.row_factory = sqlite3.Row
    return conn

@app.on_event('startup')
def startup():
    conn = get_db_connection()
    conn.execute('CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, completed INTEGER)')
    conn.commit()
    conn.close()

@app.get('/tasks')
def read_tasks():
    conn = get_db_connection()
    tasks = conn.execute('SELECT * FROM tasks').fetchall()
    conn.close()
    return [dict(t) for t in tasks]
  

Недостатки чистого SQL:

  • Ручное управление подключениями, риск утечки ресурсов.
  • Отсутствие автоматического маппинга объектов.
  • Необходимость писать SQL-запросы вручную.

Как подключить MongoDB вместо SQLite?

Для NoSQL базы данных используется pymongo. Пример вставки и чтения:


from pymongo import MongoClient

client = MongoClient('mongodb://localhost:27017/')
db = client.tododb
tasks_collection = db.tasks

@app.post('/tasks')
def create_task(task: TaskCreate):
    result = tasks_collection.insert_one(task.dict())
    new_task = tasks_collection.find_one({'_id': result.inserted_id})
    return {'id': str(new_task['_id']), 'title': new_task['title'], 'completed': new_task['completed']}
  

Проблемы перехода на MongoDB:

  • Отсутствие схемы данных, сложнее валидация.
  • Необходимость установки и настройки MongoDB сервера.
  • Отличия в синтаксисе запросов.

Расширенные примеры разработки на Python

1. Асинхронные эндпоинты в FastAPI

FastAPI поддерживает асинхронные функции. Пример асинхронного получения данных из базы через SQLAlchemy (с использованием async SQLAlchemy):

Пример

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import select

DATABASE_URL = 'sqlite+aiosqlite:///./test.db'
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

async def get_async_db():
    async with async_session() as session:
        yield session

@app.get('/async-tasks')
async def read_async_tasks(db: AsyncSession = Depends(get_async_db)):
    result = await db.execute(select(Task))
    tasks = result.scalars().all()
    return [{'id': t.id, 'title': t.title, 'completed': t.completed} for t in tasks]
  

Результат выполнения GET запроса к /async-tasks:

[{'id': 1, 'title': 'Example', 'completed': False}]

2. Обработка ошибок с помощью exception handlers

Можно переопределить стандартные обработчики ошибок для возврата единообразного ответа:

Пример

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

class CustomException(Exception):
    def __init__(self, message: str, status_code: int = 400):
        self.message = message
        self.status_code = status_code

@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
    return JSONResponse(
        status_code=exc.status_code,
        content={'detail': exc.message, 'status': 'error'}
    )

@app.get('/raise-error')
def raise_error():
    raise CustomException('Произошла ошибка', status_code=422)
  

Ответ при обращении к /raise-error:

{'detail': 'Произошла ошибка', 'status': 'error'}

3. Тестирование API с pytest и httpx

Для автоматического тестирования удобно использовать клиент httpx вместе с TestClient от FastAPI:

Пример

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_task():
    response = client.post('/tasks/', json={'title': 'Test'})
    assert response.status_code == 200
    data = response.json()
    assert data['title'] == 'Test'
    assert data['completed'] == False

def test_get_tasks():
    response = client.get('/tasks/')
    assert response.status_code == 200
    assert isinstance(response.json(), list)
  

Результат запуска тестов:

============================= test session starts ==============================
collected 2 items

test_main.py ..                                                         [100%]

============================== 2 passed in 0.25s ===============================

Пример разработки на Python - comments

En
Python пример разработки (python)