ProvenPin Install

Django Ninja API with Schema models and pagination on sqlite: working versions (tested 2026-10-15)

A typed Django Ninja CRUD API with Schema input and output models, page-number pagination and sqlite, tested offline with the ninja TestClient and pytest-django.

passed5 passed · 0 failed2026-10-15 13:20 UTC
Status
passed
Last passed
2026-10-15 13:20 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions4
django@6.1.2django-ninja@1.7.1pytest-django@4.14.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38741369695

The code

Exactly the files that passed. Your agent can pull them with get_recipe("django-ninja-schema-crud") (install), or see them on GitHub.

requirements.txt
django==6.1.2
django-ninja==1.7.1
pytest-django==4.14.0
pytest==9.1.1
settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

SECRET_KEY = "test-only-not-a-secret"
DEBUG = False
ALLOWED_HOSTS = ["testserver"]

INSTALLED_APPS = ["catalog"]

ROOT_URLCONF = "catalog.urls"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

USE_TZ = True
catalog/__init__.py
catalog/models.py
from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=200)
    year = models.PositiveIntegerField(null=True, blank=True)

    class Meta:
        # Pagination needs a stable order, otherwise pages can overlap or skip rows.
        ordering = ["id"]
catalog/api.py
from ninja import NinjaAPI, Schema
from ninja.errors import HttpError
from ninja.pagination import PageNumberPagination, paginate

from .models import Book

api = NinjaAPI()


class BookIn(Schema):
    title: str
    author: str
    year: int | None = None


class BookPatch(Schema):
    title: str | None = None
    author: str | None = None
    year: int | None = None


class BookOut(Schema):
    id: int
    title: str
    author: str
    year: int | None


def _get_book(book_id: int) -> Book:
    try:
        return Book.objects.get(pk=book_id)
    except Book.DoesNotExist:
        raise HttpError(404, "Book not found")


@api.post("/books", response={201: BookOut})
def create_book(request, payload: BookIn):
    return 201, Book.objects.create(**payload.model_dump())


@api.get("/books", response=list[BookOut])
@paginate(PageNumberPagination, page_size=2)
def list_books(request):
    return Book.objects.all()


@api.get("/books/{book_id}", response=BookOut)
def get_book(request, book_id: int):
    return _get_book(book_id)


@api.patch("/books/{book_id}", response=BookOut)
def update_book(request, book_id: int, payload: BookPatch):
    book = _get_book(book_id)
    # exclude_unset keeps fields the client did not send, instead of overwriting them with None.
    for field, value in payload.model_dump(exclude_unset=True).items():
        setattr(book, field, value)
    book.save()
    return book
catalog/urls.py
from django.urls import path

from .api import api

urlpatterns = [
    path("api/", api.urls),
]
tests/test_api.py
import pytest
from ninja.testing import TestClient

from catalog.api import api
from catalog.models import Book

client = TestClient(api)


@pytest.mark.django_db
def test_create_returns_201_and_persists():
    r = client.post("/books", json={"title": "Dune", "author": "Herbert", "year": 1965})
    assert r.status_code == 201
    assert r.json()["title"] == "Dune"
    assert Book.objects.filter(pk=r.json()["id"], year=1965).exists()


@pytest.mark.django_db
def test_pagination_returns_second_page_in_stable_order():
    for i in range(3):
        Book.objects.create(title=f"T{i}", author="A")
    r = client.get("/books?page=2")
    body = r.json()
    assert body["count"] == 3
    # page_size is 2, so page 2 holds only the third book; ordering by id makes this deterministic.
    assert [b["title"] for b in body["items"]] == ["T2"]


@pytest.mark.django_db
def test_missing_book_returns_404_with_detail():
    r = client.get("/books/999")
    assert r.status_code == 404
    assert r.json() == {"detail": "Book not found"}


@pytest.mark.django_db
def test_invalid_payload_returns_422():
    r = client.post("/books", json={"author": "no title"})
    assert r.status_code == 422


@pytest.mark.django_db
def test_partial_patch_keeps_unsent_fields():
    b = Book.objects.create(title="Keep", author="A", year=1990)
    r = client.patch(f"/books/{b.id}", json={"year": 2001})
    assert r.status_code == 200
    assert r.json()["title"] == "Keep"
    assert r.json()["year"] == 2001
pytest.ini
[pytest]
pythonpath = .
DJANGO_SETTINGS_MODULE = settings

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.