{"id":46258224,"url":"https://github.com/hoaxnerd/fastrest","last_synced_at":"2026-03-10T22:01:05.273Z","repository":{"id":325718047,"uuid":"990918130","full_name":"hoaxnerd/fastrest","owner":"hoaxnerd","description":"DRF inspired REST Framework for FastAPI.","archived":false,"fork":false,"pushed_at":"2026-03-03T22:31:41.000Z","size":5402,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-04T02:42:44.486Z","etag":null,"topics":["async","django-rest-framework","drf","fastapi","openapi","pydantic","python","rest-api","rest-framework","sqlalchemy"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/fastrest/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hoaxnerd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-05-26T21:05:46.000Z","updated_at":"2026-03-03T23:44:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"3fbae240-a019-456d-8ea4-f180bd18f269","html_url":"https://github.com/hoaxnerd/fastrest","commit_stats":null,"previous_names":["iamomi/fastrest","hoaxnerd/fastrest"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/hoaxnerd/fastrest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hoaxnerd%2Ffastrest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hoaxnerd%2Ffastrest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hoaxnerd%2Ffastrest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hoaxnerd%2Ffastrest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hoaxnerd","download_url":"https://codeload.github.com/hoaxnerd/fastrest/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hoaxnerd%2Ffastrest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30357614,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T21:41:54.280Z","status":"ssl_error","status_checked_at":"2026-03-10T21:40:59.357Z","response_time":106,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","django-rest-framework","drf","fastapi","openapi","pydantic","python","rest-api","rest-framework","sqlalchemy"],"created_at":"2026-03-04T00:26:25.857Z","updated_at":"2026-03-10T22:01:05.263Z","avatar_url":"https://github.com/hoaxnerd.png","language":"Python","readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/banner.png\" alt=\"FastREST\" width=\"600\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/hoaxnerd/fastrest/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/hoaxnerd/fastrest/actions/workflows/ci.yml/badge.svg\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pypi.org/project/fastrest/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/fastrest\" alt=\"PyPI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pypi.org/project/fastrest/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/pyversions/fastrest\" alt=\"Python\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/hoaxnerd/fastrest/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/hoaxnerd/fastrest\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n# FastREST\n\n**The REST framework that speaks to AI agents out of the box.**\n\nFastREST builds async REST APIs from your models and auto-generates MCP tools, agent skill documents, and structured API manifests — so both humans and AI agents can consume your API without extra work.\n\nBuilt on FastAPI + Pydantic. Inspired by Django REST Framework. Works with SQLAlchemy, Tortoise ORM, SQLModel, and Beanie (MongoDB).\n\n```python\nrouter.serve(Model)  # SQLAlchemy, Tortoise, SQLModel, or Beanie — any ORM model\n```\n\n```bash\npip install fastrest[sqlalchemy, mcp]  # or fastrest[tortoise], fastrest[sqlmodel], fastrest[beanie]\n```\n\nThat one line gives you:\n\n| Endpoint | What it does |\n|----------|-------------|\n| `GET /api/authors` | List, search, paginate, order |\n| `POST /api/authors` | Create with validated fields |\n| `GET /api/authors/{pk}` | Retrieve by primary key |\n| `PUT/PATCH /api/authors/{pk}` | Full or partial update |\n| `DELETE /api/authors/{pk}` | Delete (204) |\n| `GET /api/SKILL.md` | Agent-readable API documentation |\n| `GET /api/authors/SKILL.md` | Per-resource agent docs |\n| `GET /api/manifest.json` | Structured API metadata (JSON) |\n| `GET /api/mcp` | MCP server with auto-generated tools |\n| `GET /api/` | API root listing all resources |\n| `GET /docs` | Swagger UI with typed schemas |\n\n\u003e **Status:** Beta (0.1.4). Core API is stable across serializers, viewsets, routers, permissions, pagination, filtering, auth, throttling, content negotiation, and agent integration.\n\n---\n\n## Multi-ORM Support\n\nUse any Python ORM. FastREST adapts automatically:\n\n| ORM | Install | Session Required | Auto-Detected |\n|-----|---------|-----------------|---------------|\n| **SQLAlchemy** | `pip install fastrest[sqlalchemy]` | Yes | Yes (default) |\n| **Tortoise ORM** | `pip install fastrest[tortoise]` | No | Yes |\n| **SQLModel** | `pip install fastrest[sqlmodel]` | Yes | No* |\n| **Beanie** (MongoDB) | `pip install fastrest[beanie]` | No | Yes |\n\n\\* SQLModel co-installs SQLAlchemy, so auto-detection picks SQLAlchemy. Set the adapter explicitly:\n\n```python\nfrom fastrest.compat.orm import set_default_adapter\nfrom fastrest.compat.orm.sqlmodel import SQLModelAdapter\nset_default_adapter(SQLModelAdapter())\n```\n\n### Any ORM, Same API\n\nThe same `router.serve()` and `ModelViewSet` patterns work regardless of your ORM:\n\n```python\n# SQLAlchemy\nfrom sqlalchemy import Column, Integer, String\nfrom sqlalchemy.orm import DeclarativeBase\n\nclass Base(DeclarativeBase):\n    pass\n\nclass Author(Base):\n    __tablename__ = \"authors\"\n    id = Column(Integer, primary_key=True)\n    name = Column(String(200), nullable=False)\n```\n\n```python\n# Tortoise ORM — no session middleware needed\nfrom tortoise.models import Model\nfrom tortoise import fields\n\nclass Author(Model):\n    id = fields.IntField(primary_key=True)\n    name = fields.CharField(max_length=200)\n    class Meta:\n        table = \"authors\"\n```\n\n```python\n# SQLModel\nfrom sqlmodel import SQLModel, Field\n\nclass Author(SQLModel, table=True):\n    id: int | None = Field(default=None, primary_key=True)\n    name: str = Field(max_length=200)\n```\n\n```python\n# Beanie (MongoDB) — auto-detects string PK\nfrom beanie import Document\n\nclass Author(Document):\n    name: str\n    class Settings:\n        name = \"authors\"\n```\n\n```python\n# Same code for all of them\nrouter = DefaultRouter()\nrouter.serve(Author)\n```\n\n**Tortoise ORM** and **Beanie** don't require session injection middleware — they manage connections internally.\n\n**Custom adapters**: Subclass `ORMAdapter` from `fastrest.compat.orm.base` and call `set_default_adapter()`.\n\n---\n\n## AI Agent Integration\n\nFastREST is the first REST framework with **built-in agent support**. Define your viewsets once, and agents can discover and use your API automatically.\n\n### MCP Server — Tools for AI Agents\n\nMount a [Model Context Protocol](https://modelcontextprotocol.io) server with one line. Every viewset action becomes an MCP tool that agents can call directly:\n\n```python\nfrom fastrest.mcp import mount_mcp\n\nmount_mcp(app, router)\n# Auto-generates tools: authors_list, authors_create, authors_retrieve,\n#                        books_list, books_create, books_retrieve, ...\n```\n\nMCP tools run through the **full request pipeline** — authentication, permissions, and throttling all apply to agent tool calls, exactly like HTTP requests. No separate auth layer to maintain.\n\nYou must include optional \"mcp\" package while installation:\n```bash\npip install fastrest[mcp] # You should also include your preferred ORM if not already installed -\u003e fastrest[mcp, sqlmodel]\n```\n\n```python\n# Exclude specific actions from MCP\nclass BookViewSet(ModelViewSet):\n    queryset = Book\n    serializer_class = BookSerializer\n\n    @action(methods=[\"post\"], detail=True, mcp=False)  # hidden from MCP\n    async def internal_sync(self, request, **kwargs):\n        ...\n```\n\nConfigure via settings:\n\n```python\nconfigure(app, {\n    \"MCP_ENABLED\": True,\n    \"MCP_PREFIX\": \"/mcp\",\n    \"MCP_TOOL_NAME_FORMAT\": \"{basename}_{action}\",\n    \"MCP_EXCLUDE_VIEWSETS\": [\"InternalViewSet\"],\n})\n```\n\n### SKILL.md — API Documentation for Agents\n\nFastREST auto-generates Markdown skill documents that AI agents can read to understand your API. Includes fields, types, constraints, endpoints, query parameters, auth requirements, and example requests:\n\n```\nGET /api/SKILL.md            → Full API skill document\nGET /api/books/SKILL.md      → Per-resource skill document\n```\n\nThe output is a living spec — it regenerates from your code on every request, so it's always in sync with your actual API.\n\n```python\n# Customize what agents see per-viewset\nclass BookViewSet(ModelViewSet):\n    queryset = Book\n    serializer_class = BookSerializer\n    skill_description = \"Manage the book catalog. Supports search by title and ordering by price.\"\n    skill_exclude_actions = [\"destroy\"]          # hide delete from agents\n    skill_exclude_fields = [\"internal_notes\"]    # hide sensitive fields\n    skill_examples = [\n        {\n            \"description\": \"Search for Python books\",\n            \"request\": \"GET /books?search=python\",\n            \"response\": \"200\"\n        }\n    ]\n```\n\nConfigure via settings:\n\n```python\nconfigure(app, {\n    \"SKILL_ENABLED\": True,\n    \"SKILL_NAME\": \"bookstore-api\",\n    \"SKILL_BASE_URL\": \"https://api.example.com\",\n    \"SKILL_DESCRIPTION\": \"A bookstore API with full CRUD and search.\",\n    \"SKILL_AUTH_DESCRIPTION\": \"Use Bearer token in the Authorization header.\",\n    \"SKILL_INCLUDE_EXAMPLES\": True,\n    \"SKILL_MAX_EXAMPLES_PER_RESOURCE\": 3,\n})\n```\n\n### API Manifest — Machine-Readable Metadata\n\nA structured JSON endpoint at `GET /manifest.json` that describes your entire API:\n\n```json\n{\n  \"version\": \"1.0\",\n  \"name\": \"bookstore-api\",\n  \"base_url\": \"https://api.example.com\",\n  \"resources\": [\n    {\n      \"name\": \"book\",\n      \"prefix\": \"books\",\n      \"actions\": [\"list\", \"create\", \"retrieve\", \"update\", \"partial_update\", \"destroy\", \"in_stock\"],\n      \"fields\": [\n        {\"name\": \"id\", \"type\": \"integer\", \"read_only\": true},\n        {\"name\": \"title\", \"type\": \"string\", \"max_length\": 300, \"required\": true},\n        {\"name\": \"price\", \"type\": \"float\", \"required\": true}\n      ],\n      \"permissions\": [\"IsAuthenticated\"],\n      \"pagination\": {\"type\": \"PageNumberPagination\", \"page_size\": 20},\n      \"filters\": {\"search_fields\": [\"title\", \"description\"], \"ordering_fields\": [\"title\", \"price\"]}\n    }\n  ],\n  \"mcp\": {\"enabled\": true, \"prefix\": \"/mcp\"},\n  \"skills\": {\"enabled\": true, \"endpoint\": \"/SKILL.md\"}\n}\n```\n\n---\n\n## DRF-Inspired Developer Experience\n\nIf you've used Django REST Framework, you already know FastREST. Same patterns, async stack:\n\n| | DRF | FastREST |\n|---|---|---|\n| **Framework** | Django | FastAPI |\n| **ORM** | Django ORM | SQLAlchemy, Tortoise, SQLModel, Beanie |\n| **Validation** | DRF fields | DRF fields + Pydantic |\n| **Async** | No | Native async/await |\n| **OpenAPI** | Via drf-spectacular | Built-in (per-method typed routes) |\n| **Agent support** | No | MCP + SKILL.md + Manifest |\n\n---\n\n## Quick Start\n\n### Zero-Config: `router.serve()`\n\nOne line per model. Auto-generates serializers, viewsets, and routes:\n\n```python\nfrom fastapi import FastAPI\nfrom fastrest.routers import DefaultRouter\nfrom models import Author, Book, Tag\n\nrouter = DefaultRouter()\nrouter.serve(Author)                                          # → /authors, /authors/{pk}\nrouter.serve(Book, search_fields=[\"title\"], ordering_fields=[\"price\"])\nrouter.serve(Tag, readonly=True)                              # GET only\n\napp = FastAPI(title=\"My API\")\napp.include_router(router.urls, prefix=\"/api\")\n```\n\nPrefixes are auto-inferred from model names: `Author` → `authors`, `BookReview` → `book-reviews`, `Category` → `categories`.\n\n`serve()` returns the viewset class for further customization:\n\n```python\nBookViewSet = router.serve(Book,\n    exclude=[\"secret_field\"],\n    pagination_class=PageNumberPagination,\n    filter_backends=[SearchFilter, OrderingFilter],\n    search_fields=[\"title\", \"description\"],\n    ordering_fields=[\"price\", \"title\"],\n    permission_classes=[IsAuthenticated()],\n)\nBookViewSet.skill_description = \"Manage the book catalog.\"\n```\n\n**All `serve()` options**: `prefix`, `basename`, `fields`, `exclude`, `read_only_fields`, `serializer_class`, `readonly`, `viewset_class`, `permission_classes`, `authentication_classes`, `throttle_classes`, `pagination_class`, `filter_backends`, `search_fields`, `ordering_fields`, `ordering`.\n\n### Full Control: Serializer + ViewSet + Router\n\nFor complete customization, define each layer explicitly:\n\n```python\nfrom fastrest.serializers import ModelSerializer\nfrom fastrest.viewsets import ModelViewSet\nfrom fastrest.routers import DefaultRouter\n\nclass AuthorSerializer(ModelSerializer):\n    class Meta:\n        model = Author\n        fields = [\"id\", \"name\", \"bio\", \"is_active\"]\n        read_only_fields = [\"id\"]\n\nclass AuthorViewSet(ModelViewSet):\n    queryset = Author\n    serializer_class = AuthorSerializer\n\nrouter = DefaultRouter()\nrouter.register(\"authors\", AuthorViewSet, basename=\"author\")\n\napp = FastAPI()\napp.include_router(router.urls, prefix=\"/api\")\n```\n\n---\n\n## Features\n\n### Serializers\n\nModelSerializer auto-generates fields from your model and supports DRF-style validation:\n\n```python\nfrom fastrest.serializers import ModelSerializer\nfrom fastrest.fields import FloatField\nfrom fastrest.exceptions import ValidationError\n\nclass BookSerializer(ModelSerializer):\n    price = FloatField(min_value=0.01)  # override auto-generated field\n\n    class Meta:\n        model = Book\n        fields = [\"id\", \"title\", \"isbn\", \"price\", \"author_id\"]\n        read_only_fields = [\"id\"]\n\n    def validate_isbn(self, value):\n        if value and len(value) not in (10, 13):\n            raise ValidationError(\"ISBN must be 10 or 13 characters.\")\n        return value\n```\n\n**Field library:** CharField, IntegerField, FloatField, BooleanField, DecimalField, DateTimeField, DateField, TimeField, UUIDField, EmailField, URLField, SlugField, IPAddressField, DurationField, ListField, DictField, JSONField, ChoiceField, SerializerMethodField, and more.\n\n### ViewSets \u0026 Custom Actions\n\n```python\nfrom fastrest.viewsets import ModelViewSet\nfrom fastrest.decorators import action\nfrom fastrest.response import Response\n\nclass BookViewSet(ModelViewSet):\n    queryset = Book\n    serializer_class = BookSerializer\n\n    def get_serializer_class(self):\n        if self.action == \"retrieve\":\n            return BookDetailSerializer\n        return BookSerializer\n\n    @action(methods=[\"get\"], detail=False, url_path=\"in-stock\")\n    async def in_stock(self, request, **kwargs):\n        \"\"\"GET /api/books/in-stock\"\"\"\n        books = await self.adapter.filter_queryset(Book, self.get_session(), in_stock=True)\n        serializer = self.get_serializer(books, many=True)\n        return Response(data=serializer.data)\n\n    @action(methods=[\"post\"], detail=True, url_path=\"toggle-stock\",\n            mcp_description=\"Toggle the in-stock status of a book\")\n    async def toggle_stock(self, request, **kwargs):\n        \"\"\"POST /api/books/{pk}/toggle-stock\"\"\"\n        book = await self.get_object()\n        session = self.get_session()\n        await self.adapter.update(book, session, in_stock=not book.in_stock)\n        serializer = self.get_serializer(book)\n        return Response(data=serializer.data)\n```\n\nThe `@action` decorator supports: `methods`, `detail`, `url_path`, `url_name`, `serializer_class`, `response_serializer_class`, `mcp` (include in MCP tools), `mcp_description`, `skill` (include in SKILL.md).\n\n### Pagination\n\n```python\nfrom fastrest.pagination import PageNumberPagination, LimitOffsetPagination\n\nclass BookPagination(PageNumberPagination):\n    page_size = 20\n    max_page_size = 100\n\nclass BookViewSet(ModelViewSet):\n    queryset = Book\n    serializer_class = BookSerializer\n    pagination_class = BookPagination\n```\n\n```json\n{\n  \"count\": 42,\n  \"next\": \"?page=2\u0026page_size=20\",\n  \"previous\": null,\n  \"results\": [...]\n}\n```\n\nAlso available: `LimitOffsetPagination` with `?limit=20\u0026offset=0`.\n\n### Filtering \u0026 Search\n\n```python\nfrom fastrest.filters import SearchFilter, OrderingFilter\n\nclass BookViewSet(ModelViewSet):\n    queryset = Book\n    serializer_class = BookSerializer\n    filter_backends = [SearchFilter, OrderingFilter]\n    search_fields = [\"title\", \"description\", \"isbn\"]\n    ordering_fields = [\"title\", \"price\"]\n    ordering = [\"title\"]  # default ordering\n```\n\n- `GET /api/books?search=django` — case-insensitive search across `search_fields`\n- `GET /api/books?ordering=-price` — sort by price descending\n- `GET /api/books?ordering=title,price` — multi-field sort\n- All query parameters appear automatically in OpenAPI `/docs`\n\n### Permissions\n\nComposable permission classes with `\u0026`, `|`, `~` operators:\n\n```python\nfrom fastrest.permissions import BasePermission, IsAuthenticated, IsAdminUser, HasScope\n\nclass IsOwner(BasePermission):\n    def has_object_permission(self, request, view, obj):\n        return obj.owner_id == request.user.id\n\nclass ArticleViewSet(ModelViewSet):\n    queryset = Article\n    serializer_class = ArticleSerializer\n    # Compose with operators — works on instances\n    permission_classes = [IsAuthenticated() \u0026 (IsOwner() | IsAdminUser())]\n```\n\nScope-based access control:\n\n```python\nclass ArticleViewSet(ModelViewSet):\n    permission_classes = [IsAuthenticated() \u0026 HasScope(\"articles:write\")]\n```\n\nScopes are read from `request.auth.scopes` (set by your authentication backend).\n\nBuilt-in: `AllowAny`, `IsAuthenticated`, `IsAdminUser`, `IsAuthenticatedOrReadOnly`, `HasScope`.\n\n### Authentication\n\nPluggable backends, just like DRF:\n\n```python\nfrom fastrest.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication\n\ntoken_auth = TokenAuthentication(get_user_by_token=my_token_lookup)\nbasic_auth = BasicAuthentication(get_user_by_credentials=my_credentials_check)\nsession_auth = SessionAuthentication(get_user_from_session=my_session_resolver)\n\nclass ArticleViewSet(ModelViewSet):\n    queryset = Article\n    serializer_class = ArticleSerializer\n    authentication_classes = [token_auth]\n    permission_classes = [IsAuthenticated()]\n```\n\n- **`TokenAuthentication`** — `Authorization: Token \u003ckey\u003e` (or `Bearer` with `keyword=\"Bearer\"`)\n- **`BasicAuthentication`** — HTTP Basic with a callback\n- **`SessionAuthentication`** — Session-based with a callback\n\nUnauthenticated requests return **401** (not 403) when authentication backends provide `authenticate_header`.\n\n### Throttling\n\nRate-limit requests per user, IP, or custom key:\n\n```python\nfrom fastrest.throttling import SimpleRateThrottle, AnonRateThrottle, UserRateThrottle\n\nclass BurstRateThrottle(SimpleRateThrottle):\n    rate = \"60/min\"\n\n    def get_cache_key(self, request, view):\n        return f\"burst_{self.get_ident(request)}\"\n\nclass ArticleViewSet(ModelViewSet):\n    queryset = Article\n    serializer_class = ArticleSerializer\n    throttle_classes = [BurstRateThrottle()]\n```\n\n- **`AnonRateThrottle`** — Throttle unauthenticated requests by IP\n- **`UserRateThrottle`** — Throttle authenticated requests by user ID, anonymous by IP\n- Rate strings: `\"100/hour\"`, `\"10/min\"`, `\"1000/day\"`, `\"5/sec\"`\n- Throttled responses return **429** with a `Retry-After` header\n\n### App Configuration\n\nDjango-style settings per app:\n\n```python\nfrom fastrest.settings import configure\n\napp = FastAPI()\nconfigure(app, {\n    \"DEFAULT_PAGINATION_CLASS\": \"fastrest.pagination.PageNumberPagination\",\n    \"PAGE_SIZE\": 20,\n    \"DEFAULT_PERMISSION_CLASSES\": [IsAuthenticated()],\n    \"DEFAULT_AUTHENTICATION_CLASSES\": [token_auth],\n    \"DEFAULT_THROTTLE_CLASSES\": [AnonRateThrottle()],\n    \"DEFAULT_FILTER_BACKENDS\": [SearchFilter, OrderingFilter],\n    \"SKILL_NAME\": \"my-api\",\n    \"SKILL_BASE_URL\": \"https://api.example.com\",\n    \"MCP_PREFIX\": \"/mcp\",\n})\n```\n\nSettings resolve in order: **viewset attribute \u003e app config \u003e framework default**. Unknown keys raise `ValueError` by default (set `STRICT_SETTINGS=False` to allow).\n\n### Content Negotiation\n\nSelect response format based on the `Accept` header:\n\n```python\nfrom fastrest.negotiation import DefaultContentNegotiation, JSONRenderer, BrowsableAPIRenderer\n\nnegotiation = DefaultContentNegotiation()\nrenderer, media_type = negotiation.select_renderer(request, [JSONRenderer(), BrowsableAPIRenderer()])\n```\n\nSupports quality factors (`Accept: application/json;q=0.9`), format suffixes, and wildcard matching.\n\n### Validation\n\nThree levels of validation, same as DRF:\n\n```python\nclass ReviewSerializer(ModelSerializer):\n    class Meta:\n        model = Review\n        fields = [\"id\", \"book_id\", \"reviewer_name\", \"rating\", \"comment\"]\n\n    # 1. Field-level: validate_{field_name}\n    def validate_rating(self, value):\n        if not (1 \u003c= value \u003c= 5):\n            raise ValidationError(\"Rating must be between 1 and 5.\")\n        return value\n\n    # 2. Object-level: validate()\n    def validate(self, attrs):\n        if attrs.get(\"rating\", 0) \u003c 3 and not attrs.get(\"comment\"):\n            raise ValidationError(\"Low ratings require a comment.\")\n        return attrs\n\n    # 3. Field constraints via kwargs\n    # CharField(max_length=500), IntegerField(min_value=1, max_value=100)\n```\n\n### Routers\n\n```python\nfrom fastrest.routers import DefaultRouter, SimpleRouter\n\n# DefaultRouter adds API root, SKILL.md, and manifest.json\nrouter = DefaultRouter()\nrouter.register(\"authors\", AuthorViewSet, basename=\"author\")\nrouter.register(\"books\", BookViewSet, basename=\"book\")\n\n# Or use serve() for zero-config\nrouter.serve(Author)\nrouter.serve(Book, prefix=\"books\")\n\n# SimpleRouter — just the resource routes\nrouter = SimpleRouter()\n```\n\nEach HTTP method gets its own OpenAPI route with correct status codes (201 for create, 204 for delete), typed path parameters, and auto-generated Pydantic request/response schemas.\n\n### Generic Views\n\nFor when you don't need the full viewset:\n\n```python\nfrom fastrest.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView\n\nclass AuthorList(ListCreateAPIView):\n    queryset = Author\n    serializer_class = AuthorSerializer\n\nclass AuthorDetail(RetrieveUpdateDestroyAPIView):\n    queryset = Author\n    serializer_class = AuthorSerializer\n```\n\nAvailable: `CreateAPIView`, `ListAPIView`, `RetrieveAPIView`, `DestroyAPIView`, `UpdateAPIView`, `ListCreateAPIView`, `RetrieveUpdateAPIView`, `RetrieveDestroyAPIView`, `RetrieveUpdateDestroyAPIView`.\n\n### Testing\n\nBuilt-in async test client:\n\n```python\nimport pytest\nfrom fastrest.test import APIClient\n\n@pytest.fixture\ndef client(app):\n    return APIClient(app)\n\n@pytest.mark.asyncio\nasync def test_create_author(client):\n    resp = await client.post(\"/api/authors\", json={\n        \"name\": \"Ursula K. Le Guin\",\n        \"bio\": \"Science fiction author\",\n    })\n    assert resp.status_code == 201\n    assert resp.json()[\"name\"] == \"Ursula K. Le Guin\"\n\n@pytest.mark.asyncio\nasync def test_list_authors(client):\n    resp = await client.get(\"/api/authors\")\n    assert resp.status_code == 200\n    assert isinstance(resp.json(), list)\n```\n\n---\n\n## Full Example\n\nSee the [fastrest-example](https://github.com/hoaxnerd/fastrest-example) repo for a complete bookstore API with authors, books, tags, reviews, authentication, pagination, search, agent integration, and tests for SQLAlchemy, Tortoise, SQLModel, and Beanie.\n\n---\n\n## DRF Compatibility\n\n| DRF | FastREST | Status |\n|---|---|---|\n| `ModelSerializer` | `ModelSerializer` | Done |\n| `ModelViewSet` | `ModelViewSet` | Done |\n| `ReadOnlyModelViewSet` | `ReadOnlyModelViewSet` | Done |\n| `DefaultRouter` | `DefaultRouter` | Done |\n| `@action` | `@action` | Done |\n| `permission_classes` | `permission_classes` | Done |\n| `ValidationError` | `ValidationError` | Done |\n| Field library | Field library | Done |\n| `APIClient` (test) | `APIClient` (test) | Done |\n| Pagination | `PageNumberPagination`, `LimitOffsetPagination` | Done |\n| Filtering/Search | `SearchFilter`, `OrderingFilter` | Done |\n| Authentication | `TokenAuthentication`, `BasicAuthentication`, `SessionAuthentication` | Done |\n| Throttling | `SimpleRateThrottle`, `AnonRateThrottle`, `UserRateThrottle` | Done |\n| Content negotiation | `DefaultContentNegotiation`, `JSONRenderer`, `BrowsableAPIRenderer` | Done |\n| App configuration | `configure(app, settings)`, `get_settings(request)` | Done |\n| Auth scopes | `HasScope` permission class | Done |\n| — | `router.serve(Model)` — zero-config CRUD | Done |\n| — | MCP Server — AI agent tools | Done |\n| — | SKILL.md — agent skill documents | Done |\n| — | API Manifest — structured metadata | Done |\n\n---\n\n## Requirements\n\n- Python 3.10+\n- FastAPI 0.100+\n- Pydantic 2.0+\n- ORM of your choice via optional extras\n\n## License\n\nBSD 3-Clause. See [LICENSE](LICENSE).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhoaxnerd%2Ffastrest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhoaxnerd%2Ffastrest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhoaxnerd%2Ffastrest/lists"}