Tutorial

Como criar um encurtador de URL com o Django e o GraphQL

PythonDjangoPython FrameworksDevelopmentProgramming Project

O autor selecionou Girls Who Code para receber uma doação como parte do programa Write for DOnations.

Introdução

O GraphQL é um padrão de API criado pelo Facebook e de código aberto, como uma alternativa às APIs REST. Diferente das APIs REST, o GraphQL utiliza um sistema de tipos para definir sua estrutura de dados, onde todas as informações enviadas e recebidas devem estar em conformidade com um esquema pré-definido. Ele também expõe um único ponto de extremidade para todas as comunicações, em vez de várias URLs para diferentes recursos, e resolve a questão de overfetching (excesso de dados retornados), retornando somente os dados solicitados pelo cliente, gerando respostas menores e mais concisas.

Neste tutorial, você criará um back-end para um encurtador de URL (um serviço que recebe qualquer URL e gera uma versão mais curta e de mais fácil leitura), ao passo que você se aprofunda nos conceitos do GraphQL, como consultas e mutações, e ferramentas, como a interface GraphiQL. Você já deve ter usado esses serviços antes, como, por exemplo, o bit.ly.

Como o GraphQL é uma tecnologia de linguagem agnóstica, ele é implementado em diferentes linguagens e frameworks. Aqui, você usará a linguagem de programação Python de uso geral, o framework Web Django e a biblioteca Graphene-Django para a implementação do GraphQL em Python com integrações específicas para o Django.

Pré-requisitos

Passo 1 — Configurando o projeto Django

Neste passo, você instalará todas as ferramentas necessárias para o aplicativo e configurará seu projeto Django.

Assim que tiver criado seu diretório de projeto e iniciado seu ambiente virtual, conforme previsto nos pré-requisitos, instale os pacotes necessários usando o pip, o gerenciador de pacotes do Python. Este tutorial instalará a versão 2.1.7 do Django e a versão do 2.2.0 do Graphene-Django, ou uma versão mais recente:

  • pip install "django==2.1.7" "graphene-django>==2.2.0"

Agora, você tem todas as ferramentas necessárias em seu cinto de utilidades. Em seguida, crie um projeto Django usando o comando django-admin. Um projeto é o boilerplate Django padrão: um conjunto de pastas com tudo que é necessário para começar o desenvolvimento de um aplicativo Web. Neste caso, você chamará seu projeto de shorty e o criará dentro de sua pasta atual, especificando o ., no final:

  • django-admin startproject shorty .

Após criar seu projeto, execute as migrações Django. Estes arquivos contém o código Python gerado pelo Django e eles são responsáveis por alterar a estrutura do aplicativo, de acordo com os modelos Django. As alterações podem incluir a criação de uma tabela, por exemplo. Por padrão, o Django vem com seu próprio conjunto de migrações, responsável por subsistemas, como a autenticação do Django. Assim, é necessário executá-los com o seguinte comando:

  • python manage.py migrate

Este comando utiliza o intérprete Python para invocar um script Django chamado manage.py, responsável por gerenciar diferentes aspectos do seu projeto, como, por exemplo, a criação de aplicativos ou a execução de migrações.

Isso dará um resultado parecido com este:

Output
Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying sessions.0001_initial... OK

Assim que o banco de dados do Django estiver pronto, inicie seu servidor de desenvolvimento local:

  • python manage.py runserver

Isso dará:

Output
Performing system checks... System check identified no issues (0 silenced). March 18, 2020 - 15:46:15 Django version 2.1.7, using settings 'shorty.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

Este comando removerá o prompt em seu terminal e iniciará o servidor.

Visite a página http://127.0.0.1:8000 em seu navegador local. Você verá esta página:

Página inicial do servidor local do Django

Para parar o servidor e retornar ao seu terminal, pressione CTRL+C. Sempre que precisar acessar o navegador, confirme se o comando anterior está funcionando.

Em seguida, termine este passo habilitando a biblioteca Django-Graphene no projeto. O Django tem o conceito de app, um aplicativo Web com uma responsabilidade específica. Um projeto é composto por um ou vários apps. Por enquanto, abra o arquivo shorty/settings.py no editor de texto de sua escolha. Este tutorial utilizará o vim:

  • vim shorty/settings.py

O arquivo settings.py gerencia todas as configurações em seu projeto. Dentro dele, procure pela entrada INSTALLED_APPS e adicione a linha 'graphene_django':

shorty/shorty/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django',
]
...

Esta adição informa o Django que você usará um aplicativo chamado graphene-django, que foi instalado no Passo 1.

No final do arquivo, adicione a seguinte variável:

shorty/shorty/settings.py
...
GRAPHENE = {
    'SCHEMA': 'shorty.schema.schema',
}

Esta última variável aponta para seu Schema principal, que você criará mais tarde. No GraphQL, um Esquema contém todos os tipos de objeto, como recursos, consultas e mutações. Pense nele como uma documentação representando todos os dados e funcionalidades disponíveis em seu sistema.

Após as alterações, salve e feche o arquivo.

Agora, seu projeto Django está configurado. No próximo passo, você criará um aplicativo Django e os Modelos dele.

Passo 2 — Configurando um aplicativo e modelos Django

Normalmente, uma plataforma Django é composta por um projeto e muitos aplicativos, ou apps. Um aplicativo descreve um conjunto de recursos dentro de um projeto e, se for bem projetado, pode ser reutilizado por vários projetos Django.

Neste passo, você criará um aplicativo chamado shortener, responsável pela funcionalidade de encurtamento da URL em questão. Para criar a estrutura básica, digite o próximo comando em seu terminal:

  • python manage.py startapp shortener

Aqui, você usou os parâmetros startapp app_name, instruindo o manage.py a criar um aplicativo chamado shortener.

Para terminar a criação do app, abra o arquivo shorty/settings.py.

  • vim shorty/settings.py

Adicione o nome do aplicativo à mesma entrada INSTALLED_APPS, que você modificou anteriormente:

shorty/shorty/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django'
    'shortener',
]
...

Salve e feche o arquivo.

Com seu shortener adicionado ao shorty/settings.py, prossiga para a criação de modelos para seu projeto. Os Modelos constituem uma das principais funcionalidades no Django. Eles são usados para representar um banco de dados de maneira “Pythonizada”, permitindo que você gerencie, consulte e armazene dados usando o código Python.

Antes de abrir o arquivo models.py para fazer alterações, este tutorial dará uma visão geral das mudanças que você fará.

Seu arquivo modelo (shortener/models.py) terá o seguinte conteúdo, assim que tiver substituído o código existente:

shorty/shortener/models.py
from hashlib import md5

from django.db import models

Aqui, você importará os pacotes necessários para seu código. Você adicionará a linha from hashlib import md5 no topo do modelo para importar a biblioteca padrão do Python, que será usada para criar uma hash da URL. A linha from django.db import models​​​ é um auxiliar do Django para a criação de modelos.

Aviso: este tutorial refere-se ao hash como o resultado de uma função que recebe uma entrada e sempre retorna a mesma saída. Este tutorial utilizará a função hash MD5 para fins demonstrativos.

Note que a MD5 possui problemas de colisão e deve ser evitada em ambientes de produção.

Em seguida, adicione um Modelo chamado URL com os seguintes campos:

  • full_url: a URL a ser encurtada.
  • url_hash: um hash curto, representando a URL completa.
  • clicks: quantas vezes a URL encurtada foi acessada.
  • created_at: a data e hora em que a URL foi criada.
shorty/shortener/models.py
...

class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

Você gerará a url_hash aplicando o algoritmo hash MD5 ao campo full_url, usando apenas os primeiros 10 caracteres retornados pelo método save() do Modelo, executado toda vez que o Django armazena uma entrada ao banco de dados. Além disso, encurtadores de URL geralmente contam a quantidade de vezes que um link foi clicado. Você conseguirá fazer isso chamando o método clicked(), quando a URL for visitada por um usuário.

As operações mencionadas serão adicionadas em seu modelo URL com este código:

shorty/shortener/models.py
...

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        return super().save(*args, **kwargs)

Agora que você revisou o código, abra o arquivo shortener/models.py:

  • vim shortener/models.py

Substitua o código pelo seguinte conteúdo:

shorty/shortener/models.py
from hashlib import md5

from django.db import models


class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        return super().save(*args, **kwargs)

Lembre-se de salvar e fechar o arquivo.

Para aplicar estas alterações no banco de dados, será necessário criar as migrações, executando o comando a seguir:

  • python manage.py makemigrations

Isso gerará o seguinte resultado:

Output
Migrations for 'shortener': shortener/migrations/0001_initial.py - Create model URL

Depois, execute as migrações:

  • python manage.py migrate

Você verá o seguinte resultado em seu terminal:

Output
Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, shortener Running migrations: Applying shortener.0001_initial... OK

Agora que os modelos estão configurados, no próximo passo você criará o ponto de extremidade do GraphQL e uma consulta.

Passo 3 — Criando consultas

A arquitetura REST mostra recursos diferentes em pontos de extremidades distintos, cada um contendo uma estrutura de dados bem definida. Por exemplo, você pode obter uma lista de usuários em /api/users, sempre contendo os mesmos campos. O GraphQL, por outro lado, tem um único ponto de extremidade para todas as interações, e utiliza as Queries para acessar os dados. A principal (e mais valiosa) diferença é que você pode usar uma consulta para recuperar todos os seus usuários dentro de uma única solicitação.

Comece criando uma consulta para obter todas as URLs. Você precisará de algumas coisas:

  • Um tipo de URL, vinculado ao seu modelo previamente definido.
  • Uma instrução de consulta chamada urls.
  • Um método para resolver sua consulta, que significa obter todas as URLs do banco de dados e retorná-las ao cliente.

Crie um arquivo novo chamado shortener/schema.py​​​:

  • vim shortener/schema.py

Comece adicionando as instruções de import do Python:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType

from .models import URL

A primeira linha importa a biblioteca graphene principal, que contém os tipos base do GraphQL, como o List. O DjangoObjectType é um auxiliar para criar uma definição de Schema de qualquer modelo Django, e a terceira linha importa seu modelo de URL criado anteriormente.

Depois disso, crie um novo tipo do GraphQL para o modelo URL, adicionando as linhas seguintes:

shorty/shortener/schema.py
...
class URLType(DjangoObjectType):
    class Meta:
        model = URL

Por fim, adicione estas linhas para criar um tipo de consulta para o modelo URL:

shorty/shortener/schema.py
...
class Query(graphene.ObjectType):
    urls = graphene.List(URLType)

    def resolve_urls(self, info, **kwargs):
        return URL.objects.all()

Este código cria uma classe Query com um campo chamado urls, que é uma lista de URLType previamente definida. Ao resolver a consulta através do método resolve_urls, você retorna todas as URLs armazenadas no banco de dados.

O arquivo shortener/schema.py completo é mostrado aqui:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType

from .models import URL


class URLType(DjangoObjectType):
    class Meta:
        model = URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType)

    def resolve_urls(self, info, **kwargs):
        return URL.objects.all()

Salve e feche o arquivo.

Todas as consultas devem ser adicionadas ao esquema principal. Pense nele como o detentor de todos os seus recursos.

Crie um novo arquivo no caminho shorty/schema.py e abra-o em seu editor:

  • vim shorty/schema

Importe os pacotes Python seguintes, adicionando as linha a seguir. A primeira linha, como já foi referida, contém os tipos base do GraphQL. A segunda linha importa o arquivo de esquema criado anteriormente.

shorty/shorty/schema.py
import graphene

import shortener.schema

Em seguida, adicione a classe Query principal. Ela terá, através de herança, todas as consultas e operações futuras criadas:

shorty/shorty/schema.py
...
class Query(shortener.schema.Query, graphene.ObjectType):
    pass

Por último, crie a variável schema:

shorty/shorty/schema.py
...
schema = graphene.Schema(query=Query)

A configuração SCHEMA que você definiu no Passo 2 aponta para a variável schema, que acabou de criar.

O arquivo shorty/schema.py completo é mostrado aqui:

shorty/shorty/schema.py
import graphene

import shortener.schema


class Query(shortener.schema.Query, graphene.ObjectType):
    pass

schema = graphene.Schema(query=Query)

Salve e feche o arquivo.

Em seguida, habilite o ponto de extremidade do GraphQL e a interface GraphiQL, que é uma interface Web gráfica usada para interagir com o sistema GraphQL.

Abra o arquivo shorty/urls.py:

  • vim shorty/urls.py

Para fins de aprendizagem, exclua o conteúdo do arquivo e salve-o, de forma que você possa começar do zero.

As primeiras linhas que você adicionará são as instruções de importação do Python:

shorty/shorty/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

A função path é usada pelo Django para criar uma URL acessível para a interface GraphiQL. Em seguida, importe o csrf_exempt, que permite que os clientes enviem dados ao servidor. Você pode encontrar uma explicação mais completa na Documentação do Graphene. Na última linha, você importou o código atualmente responsável pela interface via GraphQLView.

Em seguida, crie uma variável chamada urlpatterns.

shorty/shorty/urls.py
...
urlpatterns = [
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Isto reunirá todo o código necessário para criar a interface GraphiQL, disponível no caminho graphql/:

O arquivo shortener/urls.py completo é mostrado aqui:

shorty/shorty/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

urlpatterns = [
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Salve o arquivo e feche-o.

De volta no terminal, execute o comando python manage.py runserver (se ainda não estiver em execução):

  • python manage.py runserver

Abra seu navegador Web no endereço http://localhost:8000/graphql​​​. Você verá esta tela:

Interface GraphiQL

O GraphiQL é uma interface onde você pode executar instruções do GraphQL e ver os resultados. Uma das características dela é a seção Docs, no canto superior direito. Como tudo no GraphQL funciona por tipos, você receberá uma documentação grátis sobre todos seus tipos, consultas, mutações, etc.

Após explorar a página, insira sua primeira consulta na área de texto principal:

query {
  urls {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

Este conteúdo mostra como uma consulta GraphQL é estruturada: primeiro, utilize a palavra-chave query para avisar o servidor que você deseja apenas alguns dados de volta. Em seguida, utilize o campo urls, definido no arquivo shortener/schema.py, dentro da classe Query. A partir daí, solicite explicitamente todos os campos definidos no modelo URL usando o estilo camel case, que é o padrão do GraphQL.

Agora, clique no botão de play no canto superior esquerdo.

Você receberá a seguinte resposta, informando que ainda não possui URLs:

Output
{ "data": { "urls": [] } }

Isso mostra que o GraphQL está funcionando. Em seu terminal, pressione CTRL+C para interromper o servidor.

Você fez muita coisa neste passo, criou o ponto de extremidade GraphQL, fez uma consulta para obter todas as URLs e habilitou a interface GraphiQL. Agora, você criará as mutações para mudar o banco de dados.

Passo 4 — Criando mutações

A maioria dos aplicativos tem uma maneira de alterar o estado de um banco de dados adicionando, atualizando ou excluindo dados. No GraphQL, essas operações são chamadas de Mutações. Elas se parecem com consultas, mas usam argumentos para enviar dados para o servidor.

Para criar sua primeira mutação, abra o shortener/schema.py:

  • vim shortener/schema.py

No final do arquivo, adicione uma nova classe chamada CreateURL:

shorty/shortener/schema.py
...
class CreateURL(graphene.Mutation):
    url = graphene.Field(URLType)

Esta classe herda o auxiliar graphene.Mutation, para ter as capacidades de uma mutação GraphQL. Ela também tem uma url de nome da propriedade, definindo o conteúdo retornado pelo servidor após a mutação terminar. Neste caso, ela será a estrutura de dados URLType.

Em seguida,adicione uma subclasse chamada Arguments à classe já definida:

shorty/shortener/schema.py
...
    class Arguments:
        full_url = graphene.String()

Isto define quais dados serão aceitos pelo servidor. Aqui, espera-se um parâmetro chamado full_url com um conteúdo String:

Adicione agora as linhas seguintes para criar o método mutate:

shorty/shortener/schema.py
...

    def mutate(self, info, full_url):
        url = URL(full_url=full_url)
        url.save()

Este método mutate faz grande parte do trabalho,pois recebe os dados do cliente e os armazena no banco de dados. No final, ele retorna a classe propriamente dita, contendo o item recém-criado.

Por último, crie uma classe Mutation para armazenar todas as mutações ao seu aplicativo, adicionando essas linhas:

shorty/shortener/schema.py
...

class Mutation(graphene.ObjectType):
    create_url = CreateURL.Field()

Até agora, você tem apenas uma mutação, chamada create_url.

O arquivo shortener/schema.py completo é mostrado aqui:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType

from .models import URL


class URLType(DjangoObjectType):
    class Meta:
        model = URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType)

    def resolve_urls(self, info, **kwargs):
        return URL.objects.all()


class CreateURL(graphene.Mutation):
    url = graphene.Field(URLType)

    class Arguments:
        full_url = graphene.String()

    def mutate(self, info, full_url):
        url = URL(full_url=full_url)
        url.save()

        return CreateURL(url=url)


class Mutation(graphene.ObjectType):
    create_url = CreateURL.Field()

Feche e salve o arquivo.

Para terminar de adicionar a mutação, mude o arquivo shorty/schema.py​​​:

  • vim shorty/schema.py

Adicione o arquivo para incluir o código destacado seguinte:

shorty/shorty/schema.py

import graphene

import shortener.schema


class Query(shortener.schema.Query, graphene.ObjectType):
    pass


class Mutation(shortener.schema.Mutation, graphene.ObjectType):
    pass


schema = graphene.Schema(query=Query, mutation=Mutation)

Salve e feche o arquivo. Caso não esteja executando o servidor local,inicie-o:

  • python manage.py runserver

Vá para http://localhost:8000/graphql em seu navegador Web. Execute sua primeira mutação na interface Web do GraphQL, executando a instrução:

mutation {
  createUrl(fullUrl:"https://www.digitalocean.com/community") {
    url {
      id
      fullUrl
      urlHash
      clicks
      createdAt
    }
  }
}

Você redigiu a mutação com o nome createURL, o argumento fullUrl e os dados que deseja na resposta definida no campo url.

O resultado terá as informações da URL que acabou de criar dentro do campo data do GraphQL, como mostrado aqui:

Output
{ "data": { "createUrl": { "url": { "id": "1", "fullUrl": "https://www.digitalocean.com/community", "urlHash": "077880af78", "clicks": 0, "createdAt": "2020-01-30T19:15:10.820062+00:00" } } } }

Com isso, foi adicionada uma URL ao banco de dados com a versão com hash dela,como você pode ver no campo urlHash. Tente executar a consulta que você criou no último Passo para ver seu resultado:

query {
  urls {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

O resultado mostrará a URL armazenada:

Output
{ "data": { "urls": [ { "id": "1", "fullUrl": "https://www.digitalocean.com/community", "urlHash": "077880af78", "clicks": 0, "createdAt": "2020-03-18T21:03:24.664934+00:00" } ] } }

Você pode também tentar executar a mesma consulta mas, dessa vez, pedindo apenas os campo que deseja.

Em seguida, tente fazer isso mais uma vez com uma URL diferente:

mutation {
  createUrl(fullUrl:"https://www.digitalocean.com/write-for-donations/") {
    url {
      id
      fullUrl
      urlHash
      clicks
      createdAt
    }
  }
}

O resultado será:

Output
{ "data": { "createUrl": { "url": { "id": "2", "fullUrl": "https://www.digitalocean.com/write-for-donations/", "urlHash": "703562669b", "clicks": 0, "createdAt": "2020-01-30T19:31:10.820062+00:00" } } } }

O sistema agora consegue criar URLs curtas e listá-las. No próximo passo, você habilitará o acesso dos usuários auma URL pela versão curta, redirecionando-os para a página correta.

Passo 5 — Criando o ponto de extremidade de acesso

Neste passo, você usará o Django Views, um método que recebe uma solicitação e retorna uma resposta, para redirecionar qualquer um que acesse o ponto de extremidade http://localhost:8000/url_hash para sua URL completa.

Abra o arquivo shortener/views.py com seu editor:

  • vim shortener/views.py

Para começar, importe dois pacotes, substituindo seus conteúdos com as linhas a seguir:

shorty/shortener/views.py
from django.shortcuts import get_object_or_404, redirect

from .models import URL

Essas linhas serão explicadas mais tarde.

Em seguida, crie um Django View chamado root. Adicione este snippet de código responsável pela visualização no final de seu arquivo:

shorty/shortener/views.py
...

def root(request, url_hash):
    url = get_object_or_404(URL, url_hash=url_hash)
    url.clicked()

    return redirect(url.full_url)

Esta linha recebe um argumento chamado url_hash da URL solicitada por um usuário. Dentro da função, a primeira linha tenta obter a URL do banco de dados usando o argumento url_hash. Caso não encontre uma URL, ela retorna ao cliente o erro HTTP 404, que significa que o recurso está indisponível. Depois disso, ele incrementa a propriedade clicked da entrada URL, certificando-se de quantificar o número de vezes que a URL foi acessada. No final, ele redireciona o cliente para a URL solicitada.

O arquivo shortener/views.py completo é mostrado aqui:

shorty/shortener/views.py
from django.shortcuts import get_object_or_404, redirect

from .models import URL


def root(request, url_hash):
    url = get_object_or_404(URL, url_hash=url_hash)
    url.clicked()

    return redirect(url.full_url)

Salve e feche o arquivo.

Em seguida, abra o <^>shorty/urls.py:

  • vim shorty/urls.py

Adicione o código destacado a seguir para habilitar a visualização root.

shorty/shorty/urls.py

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from graphene_django.views import GraphQLView

from shortener.views import root


urlpatterns = [
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
    path('<str:url_hash>/', root, name='root'),
]

A visualização root ficará acessível no caminho / de seu servidor, aceitando uma url_hash como parâmetro string.

Salve e feche o arquivo. Se não estiver executando o servidor local, inicie-o executando o comando python manage.py runserver.

Para testar sua nova adição, abra seu navegador Web e acesse a URL http://localhost:8000/077880af78. Note que a última parte da URL é o hash criado pela mutação do Passo 5. Você será redirecionado para a página URL do hash, neste caso, o site da comunidade DigitalOcean.

Agora, com o redirecionamento da URL funcionando, você tornará o aplicativo mais seguro implementando o tratamento de erros quando a mutação for executada.

Passo 6 — Implementando o tratamento de erros

O tratamento de erros é a melhor prática em todos os aplicativos, pois os desenvolvedores geralmente não controlam o que será enviado para o servidor. Neste caso, você pode tentar prever falhas e minimizar os impactos delas. Em um sistema complexo como o GraphQL, muitas coisas podem dar errado, desde o cliente pedindo dados incorretos até o servidor perdendo o acesso ao banco de dados.

Por ser um sistema de tipos, o GraphQL pode verificar tudo aquilo que o cliente solicita e recebe, através de uma operação chamada Schema Validation. Você pode ver esta operação em ação ao fazer uma consulta com um campo não existente.

Vá para http://localhost:8000/graphql em seu navegador mais uma vez e execute a próxima consulta dentro da interface GraphiQL, com o campo iDontExist:

query {
  urls {
    id
    fullUrl
    urlHash
    clicks
    createdAt
    iDontExist
  }
}

Como não existe o campo iDontExist definido em sua consulta, o GraphQL retorna uma mensagem de erro:

Output
{ "errors": [ { "message": "Cannot query field \"iDontExist\" on type \"URLType\".", "locations": [ { "line": 8, "column": 5 } ] } ] }

Isto é importante, pois no sistema de tipos GraphQL, o objetivo é enviar e receber apenas as informações já definidas no esquema.

O aplicativo, do jeito que se encontra, aceita qualquer string arbitrária no campo full_url. O problema disso é que se alguém enviar uma URL mal construída, você não estaria redirecionando o usuário para um endereço válido ao tentar acessar a informação armazenada. Neste caso, será necessário verificar se a full_url está bem formatada antes de salvá-la no banco de dados. Caso haja algum erro, acione a exceção GraphQLError com uma mensagem personalizada.

Vamos implementar esta funcionalidade em dois passos. Primeiro, abra o arquivo shortener/models.py:

  • vim shortener/models.py

Adicione as linhas destacadas na seção de importação:

shorty/shortener/models.py
from hashlib import md5

from django.db import models
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError

from graphql import GraphQLError
...

O URLValidator é um auxiliar do Django, criado para validar uma String URL, enquanto o GraphQLError é utilizado pelo Graphene para acionar exceções com uma mensagem personalizada.

Em seguida, lembre-se de validar a URL recebida pelo usuário antes de salvá-la no banco de dados. Habilite esta operação adicionando o código destacado no arquivo shortener/models.py:

shorty/shortener/models.py
class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        validate = URLValidator()
        try:
            validate(self.full_url)
        except ValidationError as e:
            raise GraphQLError('invalid url')

        return super().save(*args, **kwargs)

Primeiro, este código instancia o URLValidator na variável validate. Dentro do bloco try/except, você faz o validate() da URL recebida e aciona um GraphQLError com a mensagem personalizada invalid url, caso algo dê errado.

O arquivo shortener/models.py completo é mostrado aqui:

shorty/shortener/models.py
from hashlib import md5

from django.db import models
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError

from graphql import GraphQLError


class URL(models.Model):
    full_url = models.URLField(unique=True)
    url_hash = models.URLField(unique=True)
    clicks = models.IntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)

    def clicked(self):
        self.clicks += 1
        self.save()

    def save(self, *args, **kwargs):
        if not self.id:
            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]

        validate = URLValidator()
        try:
            validate(self.full_url)
        except ValidationError as e:
            raise GraphQLError('invalid url')

        return super().save(*args, **kwargs)

Salve e feche o arquivo. Caso não esteja executando o servidor local, inicie-o com o comando python manage.py runserver.

Em seguida, teste seu novo tratamento de erro em http://localhost:8000/graphql. Tente criar uma nova URL com uma full_url inválida na interface GraphQL:

mutation {
  createUrl(fullUrl:"not_valid_url"){
    url {
      id
      fullUrl
      urlHash
      clicks
      createdAt
    }
  }
}

Ao enviar uma URL inválida, sua exceção será acionada com a mensagem personalizada:

Output
{ "errors": [ { "message": "invalid url", "locations": [ { "line": 2, "column": 3 } ], "path": [ "createUrl" ] } ], "data": { "createUrl": null } }

Se procurar em seu terminal onde o comando python manage.py runserver está sendo executado, um erro aparecerá:

Output
... graphql.error.located_error.GraphQLLocatedError: invalid url [30/Jan/2020 19:46:32] "POST /graphql/ HTTP/1.1" 200 121

Um ponto de extremidade do GraphQL sempre falhará com um código de status HTTP 200, que geralmente significa êxito. Lembre-se de que, embora o GraphQL seja construído no topo do HTTP, ele não utiliza os conceitos do código de status do HTTP ou os métodos HTTP, como o REST faz.

Com o tratamento de erro implementado, coloque um mecanismo para filtrar suas consultas, reduzindo as informações retornadas pelo servidor.

Passo 7 — Filtros de implementação

Imagine que você começou a usar o encurtador de URL para adicionar seus próprios links. Após um tempo, existirão tantas entradas que será difícil encontrar a correta. Esse problema pode ser resolvido utilizando filtros.

O processo de filtragem é um conceito comum em APIs do REST, onde normalmente há anexado à URL um parâmetro de consulta com um campo e valor. Por exemplo, para filtrar todos os usuários chamado jojo, você poderia usar GET /api/users?name=jojo.

No GraphQL, você usará argumentos de consulta como filtros. Eles criam uma interface agradável e limpa.

Você pode resolver o problema “difícil encontrar uma URL”, permitindo que o cliente filtre as URLs pelo nome, usando o campo full_url. Para implementar isso, abra o arquivo shortener/schema.py em seu editor favorito.

  • vim shortener/schema.py

Primeiro, importe o método Q na linha destacada:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q

from .models import URL
...

Isto será usado para filtrar sua consulta do banco de dados.

Em seguida, reescreva a classe Query inteira com o seguinte conteúdo:

shorty/shortener/schema.py
...
class Query(graphene.ObjectType):
    urls = graphene.List(URLType, url=graphene.String())

    def resolve_urls(self, info, url=None, **kwargs):
        queryset = URL.objects.all()

        if url:
            _filter = Q(full_url__icontains=url)
            queryset = queryset.filter(_filter)

        return queryset
...

As modificações que estão sendo feitas são:

  • Adicione o parâmetro do filtro url dentro da variável urls e do método resolve_url.
  • Dentro de resolve_urls, se um parâmetro chamado url for dado, a filtragem do banco de dados retornará apenas as URLs que contenham o valor definido, usando o método Q(full_url__icontains=url).

O arquivo shortener/schema.py completo é mostrado aqui:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q

from .models import URL


class URLType(DjangoObjectType):
    class Meta:
        model = URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType, url=graphene.String())

    def resolve_urls(self, info, url=None, **kwargs):
        queryset = URL.objects.all()

        if url:
            _filter = Q(full_url__icontains=url)
            queryset = queryset.filter(_filter)

        return queryset


class CreateURL(graphene.Mutation):
    url = graphene.Field(URLType)

    class Arguments:
        full_url = graphene.String()

    def mutate(self, info, full_url)
        url = URL(full_url=full_url)
        url.save()

        return CreateURL(url=url)


class Mutation(graphene.ObjectType):
    create_url = CreateURL.Field()

Salve e feche o arquivo. Caso não esteja executando o servidor local, inicie-o com python manage.py runserver.

Teste suas últimas alterações em http://localhost:8000/graphql. Na interface GraphiQL, escreva a seguinte instrução. Ela filtrará todas as URLs com a palavra community:

query {
  urls(url:"community") {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

O resultado será apenas um registro, já que você adicionou somente uma URL com a string community nela. Se você tivesse adicionado mais URLs antes, o resultado poderia ter sido diferente.

Output
{ "data": { "urls": [ { "id": "1", "fullUrl": "https://www.digitalocean.com/community", "urlHash": "077880af78", "clicks": 1, "createdAt": "2020-01-30T19:27:36.243900+00:00" } ] } }

Agora, você pode pesquisar suas URLs. No entanto, se houver links demais, seus clientes podem reclamar que a lista de URL está retornando mais dados do que os aplicativos deles conseguem processar. Para resolver isso, você implementará a paginação.

Passo 8 — Implementando a paginação

Se houver entradas de URL em excesso, os clientes que utilizam seu back-end podem reclamar que o tempo de resposta está muito longo ou que o tamanho das URLs está grande demais. Até mesmo seu banco de dados pode encontrar dificuldade para reunir um enorme conjunto de informações. Para resolver este problema, permita que o cliente especifique quantos itens ele deseja dentro de cada pedido, usando uma técnica chamada paginação.

Não há modo padrão de implementar esta funcionalidade. Mesmo em APIs do REST, você pode vê-la em cabeçalhos HTTP ou em parâmetros de consulta, com diferentes nomes e comportamentos.

Neste aplicativo, você implementará a paginação, habilitando mais dois argumentos para consulta das URLs: first e skip. O first selecionará um número variável de elementos entre os primeiros da lista, e o skip especificará quantos elementos devem ser ignorados partindo do início da lista. Por exemplo, ao utilizar first == 10 e skip == 5, pegamos as primeiras 10 URLs, mas ignoramos 5 delas, retornando apenas as 5 restantes.

Implementar esta solução é parecido com a adição de um filtro.

Abra o arquivo shortener/schema.py:

  • vim shortener/schema.py

No arquivo, mude a classe Query, adicionando os dois novos parâmetros à variável urls e ao método resolve_urls, destacados no código a seguir:

shorty/shortener/schema.py
import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q

from .models import URL


class Query(graphene.ObjectType):
    urls = graphene.List(URLType, url=graphene.String(), first=graphene.Int(), skip=graphene.Int())

    def resolve_urls(self, info, url=None, first=None, skip=None, **kwargs):
        queryset = URL.objects.all()

        if url:
            _filter = Q(full_url__icontains=url)
            queryset = queryset.filter(_filter)

        if first:
            queryset = queryset[:first]

        if skip:
            queryset = queryset[skip:]

        return queryset
...

Este código usa os parâmetros recém-criados first e skip, dentro do método resolve_urls, para filtrar a consulta do banco de dados.

Salve e feche o arquivo. Caso não esteja executando o servidor local, inicie-o com python manage.py runserver.

Para testar a paginação, emita a seguinte consulta na interface GraphQL em http://localhost:8000/graphql:

query {
  urls(first: 2, skip: 1) {
    id
    fullUrl
    urlHash
    clicks
    createdAt
  }
}

Seu encurtador de URL retornará a segunda URL criada em seu banco de dados:

Output
{ "data": { "urls": [ { "id": "2", "fullUrl": "https://www.digitalocean.com/write-for-donations/", "urlHash": "703562669b", "clicks": 0, "createdAt": "2020-01-30T19:31:10.820062+00:00" } ] } }

Isto mostra que a funcionalidade de paginação funciona. Sinta-se à vontade para brincar, adicionando mais URLs e testando diferentes combinações de first e skip.

Conclusão

O ecossistema da GraphQL cresce a cada dia, com uma comunidade ativa por trás dele. A GraphQL foi testada por empresas como a GitHub e Facebook e provou-se pronta para a produção. Agora, você também pode aplicar esta tecnologia em seus próprios projetos.

Neste tutorial, você criou um serviço encurtador de URL usando o GraphQL, Python e Django, usando conceitos como consultas e mutações. Além do mais, você agora entende como se basear nessas tecnologias para desenvolver aplicativos Web, usando o framework Web Django.

Explore mais sobre o GraphQL e as ferramentas usadas aqui no site do GraphQL e os sites de documentação do Graphene. Além disso, a DigitalOcean tem tutoriais adicionais para Python e Django que você pode usar se quiser aprender mais sobre eles.

Creative Commons License