Artigo original: How to build a JSON API with Python

A especificação de APIs com JSON é uma maneira poderosa de habilitar a comunicação entre client e servidor. Ela especifica a estrutura das solicitações e respostas enviadas entre os dois, usando o formato JSON.

Como formato de dados, o JSON tem as vantagens de ser leve e legível. Isso torna muito fácil trabalhar com rapidez e produtividade. A especificação foi projetada para minimizar o número de solicitações e a quantidade de dados que precisam ser enviados entre o client e o servidor.

Aqui, você pode aprender a criar uma API em JSON básica usando Python e Flask. Em seguida, o resto do artigo mostrará como experimentar alguns dos recursos que a especificação da API em JSON tem a oferecer.

O Flask é uma biblioteca do Python que fornece um "microframework" para o desenvolvimento para a web. É ótimo para desenvolvimento rápido, pois vem com uma funcionalidade básica simples, mas extensível.

Um exemplo bastante simples de como enviar uma resposta em JSON usando Flask é vista abaixo:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def example():
   return '{"name":"Bob"}'

if __name__ == '__main__':
    app.run()

Este artigo usará dois add-ons para o Flask:

  • Flask-REST-JSONAPI ajudará a desenvolver uma API que siga de perto a especificação de APIs em JSON.
  • Flask-SQLAlchemy usará SQLAlchemy para tornar a criação e a interação com um banco de dados simples bastante rápida.

O cenário geral

O objetivo final é criar uma API que permita a interação do lado do client com um banco de dados subjacente. Haverá algumas camadas entre o banco de dados e o client – uma camada de abstração de dados e uma camada de gerenciador de recursos.

flow

Aqui temos uma visão geral das etapas envolvidas:

  1. Defina um banco de dados usando o Flask-SQLAlchemy
  2. Crie uma abstração dos dados com Marshmallow-JSONAPI
  3. Crie os gerentes de recurso com a Flask-REST-JSONAPI
  4. Crie os endpoints de URL e inicie o servidor com o Flask

Este exemplo usará um esquema simples descrevendo artistas modernos e suas relações com diferentes obras de arte.

Instale tudo

Antes de começar, você precisará configurar o projeto. Isso envolve a criação de um espaço de trabalho e ambiente virtual, a instalação dos módulos necessários e a criação dos principais arquivos do Python e do banco de dados para o projeto.

Na linha de comando, crie um diretório e navegue para ele.

$ mkdir flask-jsonapi-demo
$ cd flask-jsonapi-demo/

É bom praticar a criação de ambientes virtuais (texto em inglês) para cada um de seus projetos em Python. Você pode ignorar essa etapa, mas ela é bastante recomendada.

$ python -m venv .venv
$ source .venv/bin/activate

Uma vez que seu ambiente virtual tenha sido criado e ativado, você pode instalar os módulos necessários para este projeto.

$ pip install flask-rest-jsonapi flask-sqlalchemy

Tudo o que você precisará será instalado conforme os requisitos para essas duas extensões. Isso inclui o próprio Flask e o SQLAlchemy.

A próxima etapa é criar um arquivo Python e um banco de dados para o projeto.

$ touch application.py artists.db

Crie o esquema do banco de dados

Aqui, você começa a modificar a application.py para definir e criar o esquema do banco de dados para o projeto.

Abra application.py no seu editor de texto favorito. Comece a importar os módulos. Para ser claro, os módulos serão importados conforme avançamos.

Em seguida, crie um objeto chamado app como instância da classe Flask.

Depois, use o SQLAlchemy para conectar com o arquivo de banco de dados que você criou. A etapa final é definir e criar uma tabela chamada artists.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# Criar a aplicação em Flask
app = Flask(__name__)

# Configurar o SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////artists.db'
db = SQLAlchemy(app)

# Definir uma classe para a tabela Artist
class Artist(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    birth_year = db.Column(db.Integer)
    genre = db.Column(db.String)

# Criar a tabela
db.create_all()

Criação da camada de abstração

A próxima etapa usa o módulo Marshmallow-JSONAPI para criar uma abstração de dados lógica sobre as tabelas que acabamos de definir.

A razão para criar essa camada de abstração é simples. Ela oferece mais controle sobre como seus dados subjacentes são expostos por meio da API. Pense nessa camada como uma lente através da qual o consumidor da API pode visualizar os dados subjacentes claramente e apenas as partes que eles precisam ver.

Untitled-Diagram-Page-2

No código abaixo, a camada de abstração de dados é definida como uma classe que herda da classe Schema de Marshmallow-JSONAPI. Ela dará acesso por meio da API a registros únicos ou múltiplos da tabela artists.

Neste bloco, a classe Meta define alguns metadados. Especificamente, o nome do endpoint de URL para a interação com registros simples será artist_one, onde cada artista será identificado com um parâmetro <id> de URL. O nome do endpoint para a interação com diversos registros será artist_many.

Os demais atributos definidos referem-se às colunas na tabela artists. Aqui, você pode controlar ainda mais como cada um é exposto por meio da API.

Por exemplo, ao fazer solicitações de POST para adicionar novos artistas ao banco de dados, você pode garantir que o campo name seja obrigatório definindo required=True.

Se, por alguma razão, você não quiser que o campo birth_year seja retornado ao fazer solicitações de GET, é possível especificar isso com load_only=True.

from marshmallow_jsonapi.flask import Schema
from marshmallow_jsonapi import fields

# Criar a camada de abstração de dados
class ArtistSchema(Schema):
    class Meta:
        type_ = 'artist'
        self_view = 'artist_one'
        self_view_kwargs = {'id': '<id>'}
        self_view_many = 'artist_many'

    id = fields.Integer()
    name = fields.Str(required=True)
    birth_year = fields.Integer(load_only=True)
    genre = fields.Str()

Criar os gerentes de recursos e os endpoints de URL

A peça final do quebra-cabeça é criar um gerente de recursos e o endpoint correspondente para cada uma das rotas, /artists e /artists/id.

Cada gerente de recursos é definido como uma classe que herda das classes ResourceList e ResourceDetail da Flask-REST-JSONAPI.

Eles recebem dois atributos. schema é usado para indicar a camada de abstração de dados que o gerente de recursos utiliza, enquanto data_layer indica a sessão e o modelo de dados que será usado para a camada de dados.

Em seguida, definimos api como uma instância da classe Api de Flask-REST-JSONAPI e criamos as rotas para a API com api.route(). Esse método recebe três argumentos – a classe da camada de abstração de dados, o nome do endpoint e o caminho do URL.

A última etapa é escrever um loop principal para iniciar a aplicação no modo de depuração quando o script é executado diretamente. O modo de depuração é ótimo para desenvolvimento, mas não é adequado para execução em produção.

# Criar gerentes de recursos e endpoints

from flask_rest_jsonapi import Api, ResourceDetail, ResourceList

class ArtistMany(ResourceList):
    schema = ArtistSchema
    data_layer = {'session': db.session,
                  'model': Artist}

class ArtistOne(ResourceDetail):
    schema = ArtistSchema
    data_layer = {'session': db.session,
                  'model': Artist}

api = Api(app)
api.route(ArtistMany, 'artist_many', '/artists')
api.route(ArtistOne, 'artist_one', '/artists/<int:id>')

# loop principal para executar a aplicação em modo de depuração
if __name__ == '__main__':
    app.run(debug=True)

Fazer solicitações de GET e de POST

Agora, você pode começar a usar a API para fazer solicitações em HTTP (texto em inglês). Isso pode ocorrer por meio de um navegador da web, com uma ferramenta de linha de comando, como o curl, ou a partir de outro programa (por exemplo, um script do Python usando a biblioteca Requests).

Para iniciar o servidor, execute o script application.py:

$ python application.py

No seu navegador, vá para http://localhost:5000/artists.  Você verá um resultado em JSON de todos os registros no banco de dados até o momento. Porém, agora, o banco está vazio.

Para começar a adicionar registros ao banco de dados, você pode fazer uma solicitação de POST. Uma maneira de se fazer isso é a partir da linha de comando usando a ferramenta curl. Como alternativa, você pode usar a ferramenta Insomnia ou criar uma interface de usuário simples em HTML que publique os dados usando um formulário.

Com o curl, na linha de comando, você pode inserir o seguinte:

curl -i -X POST -H 'Content-Type: application/json' -d '{"data":{"type":"artist", "attributes":{"name":"Salvador Dali", "birth_year":1904, "genre":"Surrealism"}}}' http://localhost:5000/artists

Se você navegar até http://localhost:5000/artists, verá o registro que acaba de adicionar. Se for adicionar mais registros, eles também aparecerão aqui, pois o caminho do URL chama o endpoint artists_many.

Para ver apenas um único artista pelo id, navegue até o URL relevante. Por exemplo, para ver o primeiro artista, experimente http://localhost:5000/artists/1.

Filtragem e ordenação

Um dos recursos interessantes da especificação de APIs em JSON é a capacidade de retornar a resposta de maneiras mais úteis, definindo alguns parâmetros no URL. Por exemplo, você pode classificar os resultados de acordo com um campo escolhido ou filtrar com base em alguns critérios.

O Flask-REST-JSONAPI vem com isso incorporado.

Para ordenar os artistas por ano de nascimento, navegue para http://localhost:5000/artists?sort=birth_year. Em uma aplicação da web, isso evitaria que você precisasse ordenar os resultados do lado do client, o que demandaria bastante em termos de desempenho e causaria um impacto na experiência do usuário.

A filtragem também é fácil. Anexe ao URL os critérios pelos quais você deseja filtrar, contidos por colchetes. Há três informações para incluir:

  • "name" – o campo pelo qual você está filtrando (por exemplo, birth_year)
  • "op" - a operação de filtro ("maior que", "menor que", "igual a" etc.)
  • "val" - o valor com relação à operação de filtro (por exemplo, 1900)

O URL abaixo filtra artistas cuja data de nascimento é após 1900:

http://localhost:5000/artists?filter=[{"name":"birth_year","op":"gt","val":1900}]

Essa funcionalidade torna muito mais fácil recuperar apenas informações relevantes ao chamar a API. Isso é valioso para melhorar o desempenho, especialmente ao recuperar volumes potencialmente grandes de dados em uma conexão lenta.

Paginação

Outro recurso da especificação da API em JSON que auxilia o desempenho é a paginação. É quando grandes respostas são enviadas em várias "páginas", em vez de todas de uma só vez. Você pode controlar o tamanho da página e o número da página solicitada no URL.

Assim, por exemplo, você pode receber 100 resultados em 10 páginas em vez de carregar todos os 100 de uma só vez. A primeira página conteria resultados de 1 a 10, a segunda página conteria resultados de 11 a 20 e assim por diante.

Para especificar o número de resultados que você deseja receber por página, você pode adicionar o parâmetro ?page[size]=X para o URL, onde X é o número de resultados por página. O Flask-REST-JSONAPI usa 30 como o tamanho de página padrão.

Para solicitar um determinado número de página, você pode adicionar o parâmetro ?page[number]=X, onde X é o número da página. Você pode combinar os dois parâmetros, como vemos aqui:

http://localhost:5000/artists?page[size]=2&page[number]=2

Esse URL define o tamanho da página como sendo de dois resultados por página, pedindo pela segunda página de resultados. Isso retornará o terceiro e o quarto resultados da resposta completa.

Relacionamentos

Quase sempre, os dados em uma tabela estarão relacionados aos dados armazenados em outra. Por exemplo, se você tem uma tabela de artistas, é provável que você também queira uma tabela de obras de arte. Cada obra está relacionada ao artista que a criou.

A especificação da API em JSON permite que você trabalhe com dados relacionais facilmente. O Flask-REST-JSONAPI permite que você aproveite isso. Aqui, isso será demonstrado adicionando uma tabela de obras de arte (artworks) ao banco de dados e incluindo relacionamentos entre artista e obra de arte.

Para implementar o exemplo de obras de arte, será necessário fazer algumas alterações no código em application.py.

Primeiro, fazemos algumas importações adicionais, e criamos uma tabela que relacione cada obra de arte a um artista:

from marshmallow_jsonapi.flask import Relationship
from flask_rest_jsonapi import ResourceRelationship


# Definir a tabela Artwork
class Artwork(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String)
    artist_id = db.Column(db.Integer, 
        db.ForeignKey('artist.id'))
    artist = db.relationship('Artist',
        backref=db.backref('artworks'))

Em seguida, refazemos a camada de abstração:

# Criar a abstração de dados
class ArtistSchema(Schema):
    class Meta:
        type_ = 'artist'
        self_view = 'artist_one'
        self_view_kwargs = {'id': '<id>'}
        self_view_many = 'artist_many'

    id = fields.Integer()
    name = fields.Str(required=True)
    birth_year = fields.Integer(load_only=True)
    genre = fields.Str()
    artworks = Relationship(self_view = 'artist_artworks',
        self_view_kwargs = {'id': '<id>'},
        related_view = 'artwork_many',
        many = True,
        schema = 'ArtworkSchema',
        type_ = 'artwork')

class ArtworkSchema(Schema):
    class Meta:
        type_ = 'artwork'
        self_view = 'artwork_one'
        self_view_kwargs = {'id': '<id>'}
        self_view_many = 'artwork_many'

    id = fields.Integer()
    title = fields.Str(required=True)
    artist_id = fields.Integer(required=True)

Isso define uma camada de abstração para a tabela artwork e adiciona uma relação entre artista e obra de arte à classe ArtistSchema.

Em seguida, defina novos gerentes de recursos para acessar várias obras de arte de uma vez e uma por vez, bem como para acessar os relacionamentos entre artista e obra de arte.

class ArtworkMany(ResourceList):
    schema = ArtworkSchema
    data_layer = {'session': db.session,
                  'model': Artwork}

class ArtworkOne(ResourceDetail):
    schema = ArtworkSchema
    data_layer = {'session': db.session,
                  'model': Artwork}

class ArtistArtwork(ResourceRelationship):
    schema = ArtistSchema
    data_layer = {'session': db.session,
                  'model': Artist}

Por fim, adicionamos alguns endpoints novos:

api.route(ArtworkOne, 'artwork_one', '/artworks/<int:id>')
api.route(ArtworkMany, 'artwork_many', '/artworks')
api.route(ArtistArtwork, 'artist_artworks',
    '/artists/<int:id>/relationships/artworks')

Executamos application.py e, ao tentar publicar alguns dados a partir da linha de comando usando a ferramenta curl, temos:

curl -i -X POST -H 'Content-Type: application/json' -d '{"data":{"type":"artwork", "attributes":{"title":"The Persistance of Memory", "artist_id":1}}}' http://localhost:5000/artworks

Isso criará uma obra de arte relacionada ao artista com o id=1.

No navegador, vá para http://localhost:5000/artists/1/relationships/artworks. Isso deve mostrar as obras relacionadas ao artista com id=1. Isso evita que você escreva um URL mais complexo com parâmetros para filtrar trabalhos artísticos por seu campo artist_id. Você pode listar rapidamente todas as relações entre um determinado artista e suas obras de arte.

Outro recurso é a capacidade de incluir resultados relacionados na resposta à chamada do endpoint artists_one:

http://localhost:5000/artists/1?include=artworks

Isso retornará a resposta comum para o endpoint artists, além dos resultados para cada uma das obras desse artista.

Campos esparsos

Um último recurso que vale a pena mencionar – campos esparsos. Ao trabalhar com grandes recursos de dados com muitos relacionamentos complexos, os tamanhos de resposta podem crescer muito rapidamente. É útil recuperar apenas os campos em que você está interessado.

A especificação da API em JSON permite que você faça isso adicionando um parâmetro fields ao URL. Por exemplo, o URL abaixo obtém a resposta para um determinado artista e suas obras de arte relacionadas. No entanto, em vez de retornar todos os campos para a obra de arte fornecida, ele retorna apenas o título.

http://localhost:5000/artists/1?include=artworks&fields[artwork]=title

Isso é muito útil para melhorar o desempenho, especialmente em conexões lentas. Como regra geral, você só deve fazer solicitações de e para o servidor com a quantidade mínima de dados necessária.

Comentários finais

A especificação de APIs em JSON é um framework bastante útil para o envio de dados entre servidor e client em um formato limpo e flexível. Este artigo forneceu uma visão geral do que você pode fazer com ele, com um exemplo trabalhado em Python usando a biblioteca Flask-REST-JSONAPI.

Então, o que você vai fazer a seguir? As possibilidades são muitas. O exemplo neste artigo foi simples, com apenas duas tabelas e uma única relação entre elas. Você pode desenvolver uma aplicação tão sofisticada quanto quiser e criar uma API poderosa para interagir com a aplicação usando todas as ferramentas fornecidas aqui.

Obrigado pela leitura e uma ótima programação em Python para você!