Quickstart

It’s time to write your first advanced REST API. This guide assumes you have a working understanding of Flask, and that you have already installed both Flask and Flask-COMBO-JSONAPI. If not, then follow the steps in the Installation section.

In this section you will learn basic usage of Flask-COMBO-JSONAPI around a small tutorial that uses the SQLAlchemy data layer. This tutorial shows you an example of a person and their computers.

Advanced example

An example of Flask-COMBO-JSONAPI API looks like this:

from flask import Flask
from marshmallow import pre_load

from flask_combo_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
from flask_combo_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
from flask_combo_jsonapi.exceptions import ObjectNotFound
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from marshmallow_jsonapi.flask import Schema, Relationship
from marshmallow_jsonapi import fields

# Create the Flask application
app = Flask(__name__)
app.config['DEBUG'] = True

# Initialize SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)


# Create data storage
class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    email = db.Column(db.String)
    password = db.Column(db.String)


class Computer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    serial = db.Column(db.String)
    person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
    person = db.relationship('Person', backref=db.backref('computers'))


db.create_all()


# Create logical data abstraction (same as data storage for this first example)
class PersonSchema(Schema):
    class Meta:
        type_ = 'person'
        self_view = 'person_detail'
        self_view_kwargs = {'id': '<id>'}
        self_view_many = 'person_list'

    id = fields.Integer(as_string=True, dump_only=True)
    name = fields.String(required=True)
    email = fields.Email(load_only=True)
    display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
    computers = Relationship(
        self_view='person_computers',
        self_view_kwargs={'id': '<id>'},
        related_view='computer_list',
        related_view_kwargs={'id': '<id>'},
        many=True,
        schema='ComputerSchema',
        type_='computer',
    )

    @pre_load
    def remove_id_before_deserializing(self, data, **kwargs):
        """
        We don't want to allow editing ID on POST / PATCH

        Related issues:
        https://github.com/AdCombo/flask-combo-jsonapi/issues/34
        https://github.com/miLibris/flask-rest-jsonapi/issues/193
        """
        if 'id' in data:
            del data['id']
        return data


class ComputerSchema(Schema):
    class Meta:
        type_ = 'computer'
        self_view = 'computer_detail'
        self_view_kwargs = {'id': '<id>'}

    id = fields.Integer(as_string=True, dump_only=True)
    serial = fields.String(required=True)
    owner = Relationship(
        attribute='person',
        self_view='computer_person',
        self_view_kwargs={'id': '<id>'},
        related_view='person_detail',
        related_view_kwargs={'computer_id': '<id>'},
        schema='PersonSchema',
        type_='person',
    )

    @pre_load
    def remove_id_before_deserializing(self, data, **kwargs):
        """
        We don't want to allow editing ID on POST / PATCH

        Related issues:
        https://github.com/AdCombo/flask-combo-jsonapi/issues/34
        https://github.com/miLibris/flask-rest-jsonapi/issues/193
        """
        if 'id' in data:
            del data['id']
        return data


# Create resource managers
class PersonList(ResourceList):
    schema = PersonSchema
    data_layer = {
        'session': db.session,
        'model': Person,
    }


class PersonDetailSqlalchemyDataLayer(SqlalchemyDataLayer):

    def before_get_object(self, view_kwargs):
        if not view_kwargs.get('computer_id'):
            return
        try:
            computer = self.session.query(Computer).filter_by(
                id=view_kwargs['computer_id'],
            ).one()
        except NoResultFound:
            raise ObjectNotFound(
                "Computer: {} not found".format(view_kwargs['computer_id']),
                source={'parameter': 'computer_id'},
             )
        else:
            if computer.person is not None:
                view_kwargs['id'] = computer.person.id
            else:
                view_kwargs['id'] = None


class PersonDetail(ResourceDetail):
    schema = PersonSchema
    data_layer = {
        'session': db.session,
        'model': Person,
        'class': PersonDetailSqlalchemyDataLayer,
    }


class PersonRelationship(ResourceRelationship):
    schema = PersonSchema
    data_layer = {
        'session': db.session,
        'model': Person
    }


class RelatedComputersSqlalchemyDataLayer(SqlalchemyDataLayer):

    def query(self, view_kwargs):
        query_ = self.session.query(Computer)
        if view_kwargs.get('id') is not None:
            try:
                self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            except NoResultFound:
                raise ObjectNotFound(
                    "Person: {} not found".format(view_kwargs['id']),
                    source={'parameter': 'id'},
                )
            else:
                query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
        return query_

    def before_create_object(self, data, view_kwargs):
        if view_kwargs.get('id') is not None:
            person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
            data['person_id'] = person.id


class ComputerList(ResourceList):
    schema = ComputerSchema
    data_layer = {
        'session': db.session,
        'model': Computer,
        'class': RelatedComputersSqlalchemyDataLayer,
    }


class ComputerDetail(ResourceDetail):
    schema = ComputerSchema
    data_layer = {
        'session': db.session,
        'model': Computer,
    }


class ComputerRelationship(ResourceRelationship):
    schema = ComputerSchema
    data_layer = {
        'session': db.session,
        'model': Computer,
    }


# Create endpoints
api = Api(app)

api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/<int:id>', '/computers/<int:computer_id>/owner')
api.route(PersonRelationship, 'person_computers', '/persons/<int:id>/relationships/computers')
api.route(ComputerList, 'computer_list', '/computers', '/persons/<int:id>/computers')
api.route(ComputerDetail, 'computer_detail', '/computers/<int:id>')
api.route(ComputerRelationship, 'computer_person', '/computers/<int:id>/relationships/owner')

if __name__ == '__main__':
    # Start application
    app.run(debug=True)

This example provides the following API:

Warning

In this example Flask-SQLAlchemy is used, so you’ll need to install it before running this example.

$ pip install flask_sqlalchemy

Save this file as api.py and run it using your Python interpreter. Note that we’ve enabled Flask debugging mode to provide code reloading and better error messages.

$ python api.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader

Warning

Debug mode should never be used in a production environment!

Classical CRUD operations

Create object

Request:

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

{
  "data": {
    "type": "computer",
    "attributes": {
      "serial": "Amstrad"
    }
  }
}

Response:

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

{
  "data": {
    "attributes": {
      "serial": "Amstrad"
    },
    "id": "1",
    "links": {
      "self": "/computers/1"
    },
    "relationships": {
      "owner": {
        "links": {
          "related": "/computers/1/owner",
          "self": "/computers/1/relationships/owner"
        }
      }
    },
    "type": "computer"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/computers/1"
  }
}

List objects

Request:

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

Response:

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

{
  "data": [
    {
      "attributes": {
        "serial": "Amstrad"
      },
      "id": "1",
      "links": {
        "self": "/computers/1"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/1/owner",
            "self": "/computers/1/relationships/owner"
          }
        }
      },
      "type": "computer"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "http://localhost:5000/computers"
  },
  "meta": {
    "count": 1
  }
}

Update object

Request:

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

{
  "data": {
    "type": "computer",
    "id": "1",
    "attributes": {
      "serial": "New Amstrad"
    }
  }
}

Response:

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

{
  "data": {
    "attributes": {
      "serial": "New Amstrad"
    },
    "id": "1",
    "links": {
      "self": "/computers/1"
    },
    "relationships": {
      "owner": {
        "links": {
          "related": "/computers/1/owner",
          "self": "/computers/1/relationships/owner"
        }
      }
    },
    "type": "computer"
  },
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/computers/1"
  }
}

Delete object

Request:

DELETE /computers/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"
  }
}

Relationships

Now let’s use relationships tools.
First, create 3 computers named “Halo”, “Nestor” and “Commodore”.

Done?
Ok. So let’s continue this tutorial.

We assume that Halo has id=2, Nestor id=3 and Commodore id=4.

Update object and his relationships

Now John sell his Halo (id=2) and buys a new computer named Nestor (id=3). So we want to link this new computer to John. John have also made a mistake in his email so let’s update these 2 things in the same time.

Request:

PATCH /persons/1?include=computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": {
    "type": "person",
    "id": "1",
    "attributes": {
      "email": "john@example.com"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "type": "computer",
            "id": "3"
          }
        ]
      }
    }
  }
}

Response:

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

{
  "data": {
    "attributes": {
      "display_name": "JOHN <john@example.com>",
      "name": "John"
    },
    "id": "1",
    "links": {
      "self": "/persons/1"
    },
    "relationships": {
      "computers": {
        "data": [
          {
            "id": "3",
            "type": "computer"
          }
        ],
        "links": {
          "related": "/persons/1/computers",
          "self": "/persons/1/relationships/computers"
        }
      }
    },
    "type": "person"
  },
  "included": [
    {
      "attributes": {
        "serial": "Nestor"
      },
      "id": "3",
      "links": {
        "self": "/computers/3"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/3/owner",
            "self": "/computers/3/relationships/owner"
          }
        }
      },
      "type": "computer"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "self": "/persons/1"
  }
}

Create relationship

Now John buys a new computer named Commodore (id=4) so let’s link it to John.

Request:

POST /persons/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "4"
    }
  ]
}

Response:

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

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Relationship successfully created"
  }
}

Check person’s computers without loading actual person

Request:

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

Response:

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

{
  "data": [
    {
      "attributes": {
        "serial": "Nestor"
      },
      "id": "3",
      "links": {
        "self": "/computers/3"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/3/owner",
            "self": "/computers/3/relationships/owner"
          }
        }
      },
      "type": "computer"
    },
    {
      "attributes": {
        "serial": "Commodore"
      },
      "id": "4",
      "links": {
        "self": "/computers/4"
      },
      "relationships": {
        "owner": {
          "links": {
            "related": "/computers/4/owner",
            "self": "/computers/4/relationships/owner"
          }
        }
      },
      "type": "computer"
    }
  ],
  "jsonapi": {
    "version": "1.0"
  },
  "links": {
    "first": "http://localhost:5000/computers",
    "last": "http://localhost:5000/computers?page%5Bnumber%5D=2",
    "next": "http://localhost:5000/computers?page%5Bnumber%5D=2",
    "self": "http://localhost:5000/computers"
  },
  "meta": {
    "count": 2
  }
}

Delete relationship

Now John sells his old Nestor computer, so let’s unlink it from John.

Request:

DELETE /persons/1/relationships/computers HTTP/1.1
Content-Type: application/vnd.api+json

{
  "data": [
    {
      "type": "computer",
      "id": "3"
    }
  ]
}

Response:

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

{
  "jsonapi": {
    "version": "1.0"
  },
  "meta": {
    "message": "Relationship successfully updated"
  }
}

If you want to see more examples visit JSON API 1.0 specification