FastAPI-JSONAPI

PyPI Python Version GitHub Codecov

FastAPI-JSONAPI is an extension for FastAPI that adds support for quickly building REST APIs with huge flexibility around the JSON:API 1.0 specification. It is designed to fit the complexity of real life environments so FastAPI-JSONAPI helps you to create a logical abstraction of your data called “resource”. It can interface any kind of ORMs or data storage through the concept of data layers.

Main concepts

Architecture
* JSON:API 1.0 specification: this is a very popular specification for client-server interactions through a JSON-based REST API. It helps you work in a team because it is very precise and sharable. Thanks to this specification your API can offer a lot of features such as a strong structure of request and response, filtering, pagination, sparse fieldsets, including related objects, great error formatting, etc.

* Logical data abstraction: you usually need to expose resources to clients that don’t fit your data table architecture. For example sometimes you don’t want to expose all attributes of a table, compute additional attributes or create a resource that uses data from multiple data storages.

* Data layer: the data layer is a CRUD interface between your resource manager and your data. Thanks to this you can use any data storage or ORM. There is an already full-featured data layer that uses the SQLAlchemy ORM but you can create and use your own custom data layer to use data from your data storage. You can even create a data layer that uses multiple data storage systems and ORMs, send notifications or perform custom actions during CRUD operations.

Features

FastAPI-JSONAPI has many features:

  • Relationship management - in developing

  • Powerful filtering

  • Include related objects - in developing

  • Sparse fieldsets - in developing

  • Pagination

  • Sorting

  • Permission management - in developing

  • OAuth support - in developing

User’s Guide

This part of the documentation will show you how to get started using FastAPI-JSONAPI with FastAPI.

A minimal API

import sys
from pathlib import Path
from typing import Any, ClassVar, Dict

import uvicorn
from fastapi import APIRouter, Depends, FastAPI
from sqlalchemy import Column, Integer, Text
from sqlalchemy.engine import make_url
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.misc.sqla.generics.base import DetailViewBaseGeneric, ListViewBaseGeneric
from fastapi_jsonapi.schema_base import BaseModel
from fastapi_jsonapi.views.utils import HTTPMethod, HTTPMethodConfig
from fastapi_jsonapi.views.view_base import ViewBase

CURRENT_FILE = Path(__file__).resolve()
CURRENT_DIR = CURRENT_FILE.parent
PROJECT_DIR = CURRENT_DIR.parent.parent
DB_URL = f"sqlite+aiosqlite:///{CURRENT_DIR}/db.sqlite3"
sys.path.append(str(PROJECT_DIR))

Base = declarative_base()


class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=True)


class UserAttributesBaseSchema(BaseModel):
    name: str

    class Config:
        orm_mode = True


class UserSchema(UserAttributesBaseSchema):
    """User base schema."""


def async_session() -> sessionmaker:
    engine = create_async_engine(url=make_url(DB_URL))
    _async_session = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
    return _async_session


class Connector:
    @classmethod
    async def get_session(cls):
        """
        Get session as dependency

        :return:
        """
        sess = async_session()
        async with sess() as db_session:  # type: AsyncSession
            yield db_session
            await db_session.rollback()


async def sqlalchemy_init() -> None:
    engine = create_async_engine(url=make_url(DB_URL))
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


class SessionDependency(BaseModel):
    session: AsyncSession = Depends(Connector.get_session)

    class Config:
        arbitrary_types_allowed = True


def session_dependency_handler(view: ViewBase, dto: SessionDependency) -> Dict[str, Any]:
    return {
        "session": dto.session,
    }


class UserDetailView(DetailViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


class UserListView(ListViewBaseGeneric):
    method_dependencies: ClassVar = {
        HTTPMethod.ALL: HTTPMethodConfig(
            dependencies=SessionDependency,
            prepare_data_layer_kwargs=session_dependency_handler,
        ),
    }


def add_routes(app: FastAPI):
    tags = [
        {
            "name": "User",
            "description": "",
        },
    ]

    router: APIRouter = APIRouter()
    RoutersJSONAPI(
        router=router,
        path="/users",
        tags=["User"],
        class_detail=UserDetailView,
        class_list=UserListView,
        schema=UserSchema,
        model=User,
        resource_type="user",
    )

    app.include_router(router, prefix="")
    return tags


def create_app() -> FastAPI:
    """
    Create app factory.

    :return: app
    """
    app = FastAPI(
        title="FastAPI and SQLAlchemy",
        debug=True,
        openapi_url="/openapi.json",
        docs_url="/docs",
    )
    add_routes(app)
    app.on_event("startup")(sqlalchemy_init)
    init(app)
    return app


app = create_app()

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8080,
    )

This example provides the following API structure:

URL

method

endpoint

Usage

/users

GET

user_list

Get a collection of users

/users

POST

user_list

Create a user

/users

DELETE

user_list

Delete users

/users/{user_id}

GET

user_detail

Get user details

/users/{user_id}

PATCH

user_detail

Update a user

/users/{user_id}

DELETE

user_detail

Delete a user

Request:

POST /users HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "user",
    "attributes": {
        "name": "John"
    }
  }
}

Response:

HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

GET /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

GET /users HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "attributes": {
        "name": "John"
      },
      "id": "1",
      "links": {
        "self": "/users/1"
      },
      "type": "user"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/users"
  },
  "meta": {
    "count": 1
  }
}

Request:

PATCH /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "id": 1,
    "type": "user",
    "attributes": {
        "name": "Sam"
    }
  }
}

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "data": {
    "attributes": {
      "name": "Sam"
    },
    "id": "1",
    "links": {
      "self": "/users/1"
    },
    "type": "user"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/users/1"
  }
}

Request:

DELETE /users/1 HTTP/1.1
Content-Type: application/vnd.api+json

Response:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Object successfully deleted"
  }
}

API Reference

If you are looking for information on a specific function, class or method, this part of the documentation is for you.