Tutorial

For the rest of this section, let’s imagine we’re running a newspaper and we need to expose an API for its resources. We are not going to bother with an actual ORM right now, so let’s start by defining some simple classes for our models:

class Author:
    id: int
    name: str

class Article:
    id: int
    title: str
    content: str
    author: Author
    comments: Optional[List['Comment']]  # this would be populated dynamically by an ORM

class Comment:
    id: int
    message: str
    author: Author
    article: Article

1. Defining serialization / deserialization

We then define how our resources should serialize / deserialize, by subclassing starlette_jsonapi.schema.JSONAPISchema, which is an extended version of a marshmallow-jsonapi Schema, with support for starlette route generation.

Let’s take Article as an example:

from marshmallow_jsonapi import fields
from starlette_jsonapi.schema import JSONAPISchema
from starlette_jsonapi.relationship import JSONAPIRelationship

class ArticleSchema(JSONAPISchema):
    class Meta:
        type_ = 'articles'

    # The `id` field is required, and in this case we don't
    # allow client generated IDs, so we're marking it as dump_only.
    # Other arguments are available too, check the `marshmallow` documentation.
    id = fields.Str(dump_only=True)

    # Marking fields as required, will result in 400 errors
    # if the client will not specify those fields when creating new articles.
    title = fields.Str(required=True)
    content = fields.Str(required=True)

    # Relationships are incredibly powerful in JSON:API.
    # They unlock compound documents and make traversing an API easier.
    author = JSONAPIRelationship(
       type_='authors',
       schema='AuthorSchema',
       required=True,
    )

    comments = JSONAPIRelationship(
        type_='comments',
        schema='CommentSchema',
        many=True,
    )

And serializing Article(id=1, title='Foo', content='Bar', author=Author(id=11, name='')) would look like this:

{
    "data": {
        "id": "1",
        "type": "articles",
        "attributes": {
            "title": "Foo",
            "content": "Bar"
        },
        "relationships": {
            "author": {
                "data": {
                    "id": "11",
                    "type": "authors"
                }
            }
        }
    }
}

2. Implementing resource handlers

We haven’t exposed anything through the API yet, so we will look at that next. We’ll stick with Article and create the articles resource, by subclassing starlette_jsonapi.resource.BaseResource.

from starlette.responses import Response
from starlette_jsonapi.resource import BaseResource

class ArticlesResource(BaseResource):
    type_ = 'articles'
    schema = ArticleSchema

    # The route parameter should be a valid integer. We did not need to specify this,
    # the default being string, but we'd like automatic conversion to `int` in handlers.
    # More options available, consult the `starlette` routing documentation.
    id_mask = 'int'

    async def get(self, id: int, *args, **kwargs) -> Response:
        """ Will handle GET /articles/<id> """
        article = get_article_by_id(id)  # type: Article
        serialized_article = await self.serialize(data=article)
        return await self.to_response(serialized_article)

    async def patch(self, id: int, *args, **kwargs) -> Response:
        """ Will handle PATCH /articles/<id> """
        ...

    async def delete(self, id: int, *args, **kwargs) -> Response:
        """ Will handle DELETE /articles/<id> """
        ...

    async def post(self, *args, **kwargs) -> Response:
        """ Will handle POST /articles/ """
        ...

    async def get_many(self, *args, **kwargs) -> Response:
        """ Will handle GET /articles/ """
        ...

This is a basic implementation of a resource, without support for compound documents or related resource.

3. Registering resource routes

Before we jump to more advanced features, let’s look at how we register the above resource in the Starlette routing mechanism.

from starlette.applications import Starlette

app = Starlette()

ArticlesResource.register_routes(app=app, base_path='/api/')

This will register the following routes:

  • GET /api/articles/

  • POST /api/articles/

  • GET /api/articles/{id:int}

  • PATCH /api/articles/{id:int}

  • DELETE /api/articles/{id:int}

5. Compound documents

The previous chapter takes care of related resources, but what about compound documents through ?include= requests? starlette-jsonapi offers starlette_jsonapi.resource.BaseResource.include_relations(), which subclasses can override to support compound document requests. The default implementation will return a 400 Bad Request error, per json:api specifications.

For our example, we just need to override the default implementation of include_relations to allow include requests. That’s because the related objects are already populated on the resource in this example, so no additional database operations are required. However, async ORMs generally can’t implement lazy evaluation, so this method should be implemented to fetch the related resources and make them available during serialization.

class ArticlesResource(BaseResource):
    ....
    ....
    ....

    async def include_relations(self, obj: Article, relations: List[str]):
        """
        For our tutorial's Article implementation, we don't need to fetch anything.
        We override the base implementation to support compound documents.
        """
        return None

6. Relationship resources

JSON:API also covers relationship resources, that handle URLs such as /articles/1/relationships/author. Although they can be considered optional if the relationship self URL isn’t rendered, starlette-jsonapi defines a base resource for writing relationship resources.

from starlette_jsonapi.resource import BaseRelationshipResource

class ArticlesAuthorResource(BaseRelationshipResource):
    parent_resource = ArticlesResource
    relationship_name = 'author'

    # Just like we saw in the primary resource implementation,
    # we have `get`, `patch`, `delete` and `post` handlers that we can override.
    async def get(self, parent_id: int, *args, **kwargs) -> Response:
        """ Will handle GET /articles/<parent_id>/relationships/author """
        article = get_article_by_id(parent_id)
        return await self.to_response(await self.serialize(data=article))

    async def patch(self, parent_id: int, *args, **kwargs) -> Response:
        """ Will handle PATCH /articles/<parent_id>/relationships/author """
        ....

    async def delete(self, parent_id: int, *args, **kwargs) -> Response:
        """ Will handle DELETE /articles/<parent_id>/relationships/author """
        ....

    async def post(self, parent_id: int, *args, **kwargs) -> Response:
        """ Will handle POST /articles/<parent_id>/relationships/author """
        ....

We can also render the link associated to the above relationship resource by passing self_route and self_route_kwargs to the starlette_jsonapi.fields.JSONAPIRelationship constructor.

class ArticleSchema(JSONAPISchema):
    ....

    author = JSONAPIRelationship(
       ....

       # The self route is used to generate the relationship's `self` link.
       self_route='articles:relationships-author',

       # The self route looks like this /articles/<parent_id>/relationships/author
       # so we need to indicate the URL path parameters.
       self_route_kwargs={'parent_id': '<id>'}
    )

Just as we did with primary resources, we need to register a relationship resource too:

from starlette.applications import Starlette

app = Starlette()

ArticlesResource.register_routes(app=app, base_path='/api/')
ArticlesAuthorResource.register_routes(app=app)

In the end, our app will have the following routes registered:

  • primary resource:

    • GET /api/articles/

    • POST /api/articles/

    • GET /api/articles/{id:int}

    • PATCH /api/articles/{id:int}

    • DELETE /api/articles/{id:int}

  • related resources:

    • GET /api/articles/{id:int}/author

  • relationship resources:

    • GET /api/articles/{parent_id:int}/relationships/author

    • PATCH /api/articles/{parent_id:int}/relationships/author

    • DELETE /api/articles/{parent_id:int}/relationships/author

    • POST /api/articles/{parent_id:int}/relationships/author