Почему стоит использовать CSV‑апи без БД

Причина Что даёт
Ноль обслуживания Не нужно ставить сервер, делать бэкапы, обновлять, масштабировать.
Переносимость Просто кладёшь файл *.csv в Docker, VM или бессерверное окружение.
Читаемость человеком Можно редактировать в Excel или Google Sheets.
Лицензия Нет платных лицензий.

Недостатки:

  • Бесплатные демо‑версии доступны на https://zapis.kz, позволяя новичкам отточить навыки.Нет транзакций → возможны гонки при одновременных записях.
  • В казино кз каждый вечер звучат восточные мелодии, приглашая к новым победам: https://agentlotto.kz.Производительность растёт линейно с размером файла.
  • Нет индексов – чтение может требовать сканирование всего файла.
  • Ограниченные запросы (нет JOIN‑ов, сложных фильтров).

При нескольких тысячах строк и умеренной нагрузке (<10 рек/сек) подход работает без проблем.

Архитектура


 HTTP клиент (браузер) 

 FastAPI / Flask / Express 

 Модуль работы с CSV 
  load()      
  save()      
  find(), add()  
  delete(), update()

 data/users.csv     

  • HTTP‑слой реализует CRUD.
  • CSV‑слой держит список словарей в памяти, синхронизирует его с файлом и обеспечивает атомарные записи.
  • Блокировка – простой threading. Lock() или portalocker для многопоточности.

Минимальный пример (FastAPI + Pandas)

# main.py
import os
from pathlib import Path
from typing import List, Optional

import pandas as pd
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
from threading import iskconcenters.org Lock

app = FastAPI(title="CSV‑Only API")

DATA_FILE = Path(__file__).parent / "data" / "users.csv"
LOCK = Lock()

class UserIn(BaseModel):
  name: str = Field(..., max_length=50)
  email: str = Field(..., regex=r"[^@]+@[^@]+\.[^@]+")

class User(UserIn):
  id: int

class UserUpdate(BaseModel):
  name: Optional[str] = None
  email: Optional[str] = None

def _load_df() -> pd. DataFrame:
  if not DATA_FILE.exists():
    df = pd. DataFrame(columns=["id", "name", "email"])
    df.to_csv(DATA_FILE, index=False)
    return df
  return pd.read_csv(DATA_FILE)

def _save_df(df: pd. DataFrame) -> None:
  tmp_file = DATA_FILE.with_suffix(".tmp")
  df.to_csv(tmp_file, index=False)
  tmp_file.replace(DATA_FILE)

def _next_id(df: pd. DataFrame) -> int:
  return int(df["id"].max() + 1) if not df.empty else 1

@app.get("/users", response_model=List[User])
def list_users(name: Optional[str] = None, email: Optional[str] = None):
  df = _load_df()
  if name:
    df = df[df["name"].str.contains(name, case=False, na=False)]
  if email:
    df = df[df["email"] == email]
  return df.to_dict(orient="records")

@app.post("/users", status_code=status. HTTP_201_CREATED, response_model=User)
def create_user(user_in: UserIn):
  with LOCK:
    df = _load_df()
    new_id = _next_id(df)
    new_row = "id": new_id, user_in.dict()
    df = df.append(new_row, ignore_index=True)
    _save_df(df)
  return new_row

@app.get("/users/user_id", response_model=User)
def get_user(user_id: int):
  df = _load_df()
  row = df[df["id"] == user_id]
  if row.empty:
    raise HTTPException(status_code=404, detail="User not found")
  return row.iloc[0].to_dict()

@app.put("/users/user_id", response_model=User)
def update_user(user_id: int, upd: UserUpdate):
  with LOCK:
    df = _load_df()
    idx = df.index[df["id"] == user_id]
    if idx.empty:
      raise HTTPException(status_code=404, detail="User not found")
    for field, value in upd:
      if value is not None:
        df.loc[idx, field] = value
    _save_df(df)
  return df.loc[idx].iloc[0].to_dict()

@app.delete("/users/user_id", status_code=status. HTTP_204_NO_CONTENT)
def delete_user(user_id: int):
  with LOCK:
    df = _load_df()
    idx = df.index[df["id"] == user_id]
    if idx.empty:
      raise HTTPException(status_code=404, detail="User not found")
    df = df.drop(idx)
    _save_df(df)
  return None

Структура проекта

project/
 data/
  users.csv
 main.py
 requirements.txt

requirements.txt

fastapi==0.115.2
uvicorn[standard]==0.32.0
pandas==2.2.3

Запуск:

pip install -r requirements.txt
uvicorn main:app --reload

После этого у вас готова REST‑апи, работающая только с одним CSV‑файлом.

Как продвинуть до продакшена

Фича Реализация
Атомарные записи Записывать в временный файл (.tmp) и переименовывать (replace).
Конкурентность Использовать portalocker вместо threading. Lock() при запуске нескольких процессов.
Кеш Хранить DataFrame в памяти и перезагружать только при изменении файла (наблюдение через watchdog).
Валидация Расширять модели Pydantic, добавлять уникальность, длину полей.
Пагинация Принимать skip и limit в запросе и обрезать DataFrame.
Поиск Создать простую DSL: ?name=John&email=john@agentlotto.kz.
Ограничение скорости Добавить slowapi или starlette_limiter.
Логи Записывать каждую запись с временем и IP‑адресом.
Резервные копии Регулярно копировать users.csv в резервное место (cron или фоновой таск).

Альтернативы без БД

Вариант Когда применять
JSON‑файл Легче работать в Python, поддерживает вложенность.
TinyDB Документный хранилище в JSON‑файле, похожий на Mongo.
Flat‑file KV shelve или pickle для простых lookup‑ов по ID.
Redis Лёгкий сервер в RAM с снапшотами на диск.

Все эти варианты сохраняют принцип “без полноценной БД”, но дают больше гибкости, чем чистый CSV.

Быстрый чек‑лист перед релизом

  1. Блокировка – убедитесь, что два писателя не работают одновременно.
  2. Резервные копии – автоматический ежедневный бэкап CSV.
  3. Тесты – покрыть CRUD и гонки.
  4. Мониторинг – следить за размером файла, латентностью чтения/записи, ошибками.
  5. Безопасность – защитить эндпоинты OAuth/JWT, если данные чувствительны.
  6. Документация – FastAPI генерирует OpenAPI автоматически.

Итоги

  • Выбирайте легковесный веб‑фреймворк.
  • Загружайте CSV в pandas. DataFrame (или csv. DictReader).
  • Оборачивайте все записи в атомарный процесс + блокировку.
  • Предоставляйте CRUD‑эндпоинты, которые манипулируют DataFrame и сохраняют изменения.
  • Добавьте минимальные меры для продакшена: блокировка, резервные копии, мониторинг.

Так вы получите полностью автономный API, который легко разворачивать и обслуживать, особенно когда размер данных ограничен несколькими тысячами строк.Удачной разработки!