One place for hosting & domains

      Cómo crear un acortador de URL con Django y GraphQL


      El autor seleccionó Girls Who Code para recibir una donación como parte del programa Write for DOnations.

      Introducción

      GraphQL es un estándar de API de código abierto creado por Facebook como alternativa a las API REST. En vez de las API REST, GraphQL utiliza un sistema escrito para definir su estructura de datos, donde toda la información enviada y recibida debe cumplir con un Schema predefinido. También expone un endpoint individual para todas las comunicaciones en vez de múltiples URLs para diferentes recursos y resuelve el problema de overfetching devolviendo solo los datos pedidos por el cliente, generando así respuestas más pequeñas y concisas.

      En este tutorial creará un backend para un servicio acortador de URL que toma cualquier URL y genera una versión más corta y legible. Además profundizaremos en los conceptos de GraphQL, como “Query” y “Mutation”, y en las herramientas como la interfaz GraphiQL. Es posible que ya haya usado dichos servicios antes, como bit.ly.

      Ya que GraphQL es un lenguaje con tecnología agnóstica, se implemente sobre varios lenguajes y marcos. Aquí, usará el lenguaje de programación Python de uso general, el marco web Django, y la biblioteca Graphene-Django como la implementación de GraphQL Python con integraciones específicas para Django.

      Requisitos previos

      Paso 1: Configurar el proyecto Django

      En este paso, instalará todas las herramientas necesarias para la aplicación y para configurar su proyecto de Django.

      Una vez que haya creado el directorio de su proyecto e iniciado su entorno virtual, como se indica en los requisitos previos, instale los paquetes necesarios usando pip, el administrador de paquetes de Phython. Este tutorial instalará la versión 2.1.7 de Django y la versión 2.2.0 de Graphene-Django o superior.

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

      Ahora tendrá todas las herramientas necesarias para trabajar. A continuación, creará un proyecto Django usando el comando django-admin. Un proyecto es la plantilla predeterminada de Django, un conjunto de carpetas y archivos con todo lo necesario para iniciar el desarrollo de una aplicación web. En este caso, llamará a su proyecto shorty y lo creará dentro de su carpeta especificando el . al final:

      • django-admin startproject shorty .

      Tras crear su proyecto, ejecutará las migraciones de Django. Estos archivos contienen código de Python generado por Django y se encargan de cambiar la estructura de la aplicación según los modelos de Django. Los cambios pueden incluir la creación de una tabla, por ejemplo. Por defecto, Django cuenta con su propio conjuntos de migraciones responsables de los subsistemas como la Autenticación de Django, de forma que es necesario ejecutarlos con el siguiente comando:

      Este comando utiliza el intérprete de Python para invocar una secuencia de comandos llamada manage.py, responsable de gestionar los diferentes aspectos de su proyecto, como crear aplicaciones o ejecutar migraciones.

      Con esto, se mostrará un resultado similar al siguiente:

      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

      Una vez que la base de datos de Django esté lista, inicie su servidor de desarrollo local:

      • python manage.py runserver

      Esto proporcionará lo siguiente:

      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 eliminará la instrucción en su terminal e iniciará el servidor.

      Visite la página http://127.0.0.1:8000 en su navegador local. Verá esta página:

      Página frontal del servidor local de Django

      Para detener el servidor y volver a su terminal, pulse CTRL+C. Siempre que necesite acceder al navegador, asegúrese de que se esté ejecutando el comando anterior.

      A continuación, terminará este paso habilitando la biblioteca Django-Graphene en el proyecto. Django tiene el concepto de app, una aplicación web con una responsabilidad específica. Un proyecto se compone de una o múltiples aplicaciones. Por ahora, abra el archivo shorty/settings.py​​​ en el editor de texto que prefiera. En este tutorial usaremos vim:

      El archivo settings.py gestiona todos los ajustes de su proyecto. Dentro, busque la entrada INSTALLED_APPS y añada la línea '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 adición indica a Django que usará una aplicación llamada graphene_django, que instaló en el paso 1.

      Al final del archivo, añada la siguiente variable:

      shorty/shorty/settings.py

      ...
      GRAPHENE = {
          'SCHEMA': 'shorty.schema.schema',
      }
      

      Esta última variable apunta a su esquema principal. Lo creará más tarde. En GraphQL, un Schema contiene todos los tipos de objeto, como “Resource”, “Query” y “Mutation”. Piense en ello como la documentación que representa todos los datos y funcionalidades disponibles en su sistema.

      Tras realizar las modificaciones, guarde y cierre el archivo.

      Con esto, habrá configurado el proyecto Django. En el siguiente paso, creará una aplicación Django y sus Modelos.

      Paso 2: Configurar una aplicación y modelos Django

      Una plataforma Django está normalmente compuesta de un proyecto y muchas aplicaciones o apps. Una app describe un conjunto de funciones dentro de un proyecto, y, si está bien diseñada, puede reutilizarse en varios proyectos Django.

      En este paso, creará una app llamada shortener, responsable de la función real de acortamiento de URL. Para crear su esqueleto básico, escriba el siguiente comando en su terminal:

      • python manage.py startapp shortener

      Aquí usó los parámetros startapp app_name​​​, que indican a manage.py que cree una app llamada shortener.

      Para terminar de crear la app, abra el archivo shorty/settings.py​​​.

      Añada el nombre de la app a la misma entrada INSTALLED_APPS que modificó antes:

      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',
      ]
      ...
      

      Guarde y cierre el archivo.

      Una vez que se añada su shortener a shorty/settings.py, podrá proceder a crear los modelos para su proyecto. Los modelos son una de las funciones clave de Django. Se usan para representar una base de datos “al estilo de Phyton”, lo cual le permite administrar, consultar y almacenar datos usando código Python.

      Antes de abrir el archivo models.py para realizar cambios, en este tutorial se le proporcionará una descripción general de los cambios que realizará.

      Su archivo modelo, shortener/models.py, contendrá el siguiente contenido una vez que haya sustituido el código existente:

      shorty/shortener/models.py

      from hashlib import md5
      
      from django.db import models
      

      Aquí importará los paquetes requeridos que su código necesita. Añadirá la línea from hashlib import md5 encima para importar la biblioteca estándar Python que se usará para crear un hash de la URl. La línea from django.db import models es un elemento auxiliar de Django para crear modelos.

      Advertencia: Este tutorial se refiere a hash como el resultado de una función que toma una entrada y siempre devuelve el mismo resultado. Este tutorial usará la función hash MD5 con fines demostrativos.

      Observe que MD5 tiene problemas de colisión y debería evitarse en producción.

      A continuación, añadirá un modelo llamado URL con los siguientes campos:

      • full_url: la URL a acortar.
      • url_hash: un hash corto que representa la URL completa.
      • clicks: cuántas veces se accedió a la URL corta.
      • created_at: la fecha y hora en la que se creó la URL.

      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)
      

      Generará la url_hash aplicando el algoritmo hash MD5 al campo full_url y usando solo los 10 primeros caracteres obtenidos durante el método save() del modelo, que se ejecuta cada vez que Django guarda una entrada en la base de datos. Adicionalmente, los acortadores de URL normalmente realizan un seguimiento de la cantidad de clics que se hacen sobre un enlace. Conseguirá esto invocando el método clicked() cuando un usuario visita una URL.

      Las operaciones mencionadas se añadirán dentro de su modelo URL con 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)
      

      Ahora que ha revisado el código, abra el archivo shortener/models.py:

      Sustituya el código con el siguiente contenido:

      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)
      

      Asegúrese de guardar y cerrar el archivo.

      Para aplicar estos cambios en la base de datos, deberá crear las migraciones ejecutando el siguiente comando:

      • python manage.py makemigrations

      Esto generará el siguiente resultado:

      Output

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

      Luego ejecute las migraciones:

      Verá el siguiente resultado en su terminal:

      Output

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

      Ahora que ha configurado los modelos, en el siguiente paso creará el endpoint GraphQL y una “Query”.

      Paso 3: Crear consultas

      La arquitectura REST expone diferentes recursos en diferentes extremos, cada uno con una estructura de datos bien definida. Por ejemplo, puede recuperar una lista de usuarios en /api/users, esperando siempre los mismos campos. GraphQL, por otro lado, tiene un endpoint único para todas las interacciones, y utiliza Query para acceder a los datos. La principal diferencia, y la más valiosa, es que puede usar una “Query” para recuperar todos los usuarios en una única solicitud.

      Comience creando una “Query” para recuperar todas las URL. Necesitará algunos elementos:

      • Un tipo de URL, vinculado a su modelo definido previamente.
      • Una instrucción “Query” llamada urls.
      • Un método para resolver su “Query”, lo que implica obtener todas las URL de la base de datos y devolverlas al cliente.

      Cree un nuevo archivo llamado shortener/shema.py:

      Comience añadiendo las instrucciones import de Python:

      shorty/shortener/schema.py

      import graphene
      from graphene_django import DjangoObjectType
      
      from .models import URL
      

      La primera línea importa la biblioteca principal graphene, que contiene los tipos GraphQL básicos, como List. DjangoObjectType es un elemento auxiliar para crear una definición de esquema desde cualquier modelo de Django y la tercera línea importa su modelo URL creado previamente.

      Tras eso, cree un nuevo tipo de GraphQL para el modelo URL añadiendo las siguientes líneas:

      shorty/shortener/schema.py

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

      Finalmente, añada estas líneas para crear un tipo de “Query” para el 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 crea una clase Query con un campo llamado urls, que es una lista del URLType definido previamente. Cuando se resuelve la “Query” a través del método resolve_urls, muestra las URL almacenadas en la base de datos.

      El archivo completo shorterner/schema.py se muestra aquí:

      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()
      

      Guarde y cierre el archivo.

      Todas las “Query” deben añadirse ahora al esquema principal. Piense en él como un depósito para todos sus recursos.

      Cree un nuevo archivo en la ruta shorty/schema.py​​​ y ábralo con su editor:

      Importe los siguientes paquetes de Python añadiendo las líneas que se muestran a continuación. La primera, como se ha mencionado, contiene los tipos GraphQL básicos. La segunda línea importa el archivo de esquema creado previamente.

      shorty/shorty/schema.py

      import graphene
      
      import shortener.schema
      

      A continuación, añada la clase Query principal. Contendrá, mediante herencia, todas las “Query” para las operaciones futuras creadas:

      shorty/shorty/schema.py

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

      Por último, cree la variable schema:

      shorty/shorty/schema.py

      ...
      schema = graphene.Schema(query=Query)
      

      El ajuste SCHEMA que definió en el paso 2 apunta a la variable schema que acaba de crear.

      El archivo completo shorty/schema.py se muestra aquí:

      shorty/shorty/schema.py

      import graphene
      
      import shortener.schema
      
      
      class Query(shortener.schema.Query, graphene.ObjectType):
          pass
      
      schema = graphene.Schema(query=Query)
      

      Guarde y cierre el archivo.

      A continuación, habilite el extremo GraphQL y la interfaz GraphiQL, una interfaz web gráfica que se usa para interactuar con el sistema GraphQL.

      Abra el archivo shorty/urls.py:

      Con fines de aprendizaje, elimine el contenido del archivo y guárdelo, para que pueda comenzar desde cero.

      Las primeras líneas que añadirá son las instrucciones de importación de Python:

      shorty/shorty/urls.py

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

      Django utiliza la función path para crear una URL accesible para la interfaz GraphiQL. A continuación, importe csrf_exempt, que permite a los clientes enviar datos al servidor. Puede encontrar una explicación completa en la Documentación de Graphene. En la última línea, importó el código real responsable de la interfaz a través de GraphQLView.

      A continuación, cree una variable llamada urlpatterns.

      shorty/shorty/urls.py

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

      Esto unirá todo el código necesario para hacer que la interfaz GraphiQL esté disponible en la ruta graphql/:

      El archivo completo shorterner/urls.py se muestra aquí:

      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))),
      ]
      

      Guarde el archivo y ciérrelo.

      De vuelta en el terminal, ejecute el comando python manage.py runserver (si no se está ejecutando aún):

      • python manage.py runserver

      Abra su navegador web en la dirección http://localhost:8000/graphql. Se le presentará esta pantalla:

      Interfaz GraphiQL

      GraphiQL es una interfaz donde puede ejecutar instrucciones de GraphQL y ver los resultados. Una función es la sección Docs en la parte superior derecha. Debido a que en GraphQL se debe escribir todo, obtendrá documentación gratuita sobre sus “Type”(tipo), “Query” (consulta) y “Mutation” (mutación), entre otros aspectos.

      Tras explorar la página, inserte su primera “Query” en el área de texto principal:

      query {
        urls {
          id
          fullUrl
          urlHash
          clicks
          createdAt
        }
      }
      

      Este contenido muestra cómo se estructura una “Query” de GraphQL: primero utiliza la palabra clave query para indicar al servidor que solo desea recuperar algunos de los datos. A continuación, usará el campo urls definido en el archivo shortener/schema.py dentro de la clase Query. Desde ahí, solicita explícitamente todos los campos definidos en el modelo URL usando el estilo de capitalización camel, que es el predeterminado para GraphQL.

      Ahora, haga clic en el botón de flecha de reproducción en la parte superior izquierda.

      Recibirá la siguiente respuesta, indicando que aún no tiene URLs:

      Output

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

      Esto muestra que GraphQL está funcionando. En su terminal, pulse CTRL+C para detener su servidor.

      En este paso, logró un gran avance al crear el extremo de GraphQL, realizar una “Query” para obtener todas las URLs y habilitar la interfaz de GraphiQL. Ahora, creará “Mutation” para cambiar la base de datos.

      Paso 4: Crear mutaciones

      La mayoría de las aplicaciones tienen una forma de cambiar el estado de la base de datos añadiendo, actualizando o eliminando datos. En GraphQL, estas operaciones se denominan Mutation. Tienen el aspecto de las “Query” pero utilizan argumentos para enviar datos al servidor.

      Para crear su primera “Mutation”, abra shortener/schema.py:

      Al final del archivo, comience añadiendo una nueva clase llamada CreateURL:

      shorty/shortener/schema.py

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

      Esta clase hereda el elemento auxiliar graphene.Mutation para tener las capacidades de una mutación GraphQL. También tiene un nombre de propiedad url, que define el contenido mostrado por el servidor tras completarse la “Mutation”. En este caso, será una estructura de datos URLType.

      A continuación añada una subclase llamada Arguments a la clase ya definida:

      shorty/shortener/schema.py

      ...
          class Arguments:
              full_url = graphene.String()
      

      Esto define qué datos serán aceptados por el servidor. Aquí está esperando un parámetro llamado full_url con un contenido String:

      Ahora añada las siguientes líneas para crear el 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 hace gran parte del trabajo al recibir los datos del cliente y guardarlos en la base de datos. Al final, muestra la clase que contiene el elemento recién creado.

      Por último, cree una clase Mutation para albertar todas las “Mutation” para su app añadiendo estas líneas:

      shorty/shortener/schema.py

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

      Hasta ahora, solo tendrá una mutación llamada create_url.

      El archivo completo shorterner/schema.py se muestra aquí:

      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()
      

      Cierre y guarde el archivo.

      Para terminar de añadir la “Mutation”, cambie el archivo shorty/schema.py:

      Altere el archivo para incluya el siguiente código resaltado:

      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)
      

      Guarde y cierre el archivo. Si no está ejecutando el servidor local, inícielo:

      • python manage.py runserver

      Navegue a http://localhost:8000/graphql en su navegador web. Ejecute su primera “Mutation” en la interfaz web GrapiQL ejecutando la siguiente instrucción:

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

      Compuso la “Mutation” con el nombre createURL, el argumento fullUrl y los datos que desea en la respuesta definida dentro del campo url.

      El resultado contendrá la información de la URL que acaba de crear dentro del campo de GraphQL data, como se muestra aquí:

      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" } } } }

      Con eso, se añadió una URL a la base de datos con su versión en hash, como puede ver en el campo urlHash. Intente ejecutar la “Query” que creó en el último paso para ver su resultado:

      query {
        urls {
          id
          fullUrl
          urlHash
          clicks
          createdAt
        }
      }
      

      El resultado mostrará la URL almacenada:

      Output

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

      También puede intentar ejecutar la misma “Query”, pero solo pidiendo los campos que desea.

      A continuación, inténtelo una vez más con una URL diferente:

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

      El resultado será lo siguiente:

      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" } } } }

      El sistema ahora puede crear URLs cortas y listarlas. En el siguiente paso, permitirá a los usuarios acceder a una URL por medio de su versión corta y los redirigirá a la página correcta.

      Paso 5: Crear el endpoint de acceso

      En este paso, usará el método Django Views, un método que toma una solicitud y devuelve una respuesta, para redirigir a cualquiera que acceda al endpoint http://localhost:8000/url_hash a su URL completa.

      Abra el archivo shortener/views.py con su editor:

      Para comenzar, importe los dos paquetes sustituyendo el contenido con las siguientes líneas:

      shorty/shortener/views.py

      from django.shortcuts import get_object_or_404, redirect
      
      from .models import URL
      

      Estas se explicarán más detenidamente más adelante.

      A continuación, creará un Django View llamado root. Añada este snippet de código responsable de View al final del archivo:

      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)
      

      Esto recibe un argumento llamado url_hash desde la URL solicitada por un usuario. Dentro de la función, la primera línea intenta obtener la URL de la base de datos usando el argumento url_hash. Si no se encuentra, devuelve el error HTTP 404 al cliente, lo que significa que falta el recurso. Después, aumenta la propiedad clicked de la entrada de URL, asegurando que realiza un seguimiento de la cantidad de veces que se accede a la URL. Al final, redirige el cliente a la URL solicitada.

      El archivo completo shorterner/views.py se muestra aquí:

      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)
      

      Guarde y cierre el archivo.

      A continuación, abra shorty/urls.py:

      Añada el siguiente código resaltado para habilitar la vista 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'),
      ]
      

      La vista root será accesible en la ruta / de su servidor, aceptando un url_hash como parámetro de la cadena.

      Guarde y cierre el archivo. Si no está ejecutando el servidor local, inícielo ejecutando el comando python manage.py runserver.

      Para probar su nueva adición, abra su navegador web y acceda a la URL http://localhost:8000/077880af78. Observe que la última parte de la URL es el hash creado por la “Mutation” del paso 5. Accederá a la página URL del hash; en este caso, el sitio web de la comunidad de DigitalOcean.

      Ahora que el redireccionamiento de la URL funciona, hará que la aplicación sea más segura implementando la gestión de errores cuando se ejecute la “Mutation”.

      Paso 6: Implementar la gestión de errores

      Gestionar errores es una práctica recomendada en todas las aplicaciones, ya que los desarrolladores normalmente no controlan lo que se enviará al servidor. En este caso, puede intentar prever los fallos y minimizar sus impactos. En un sistema complejo como GraphQL, pueden salir mal muchas cosas, desde que el cliente pida los datos erróneos erróneos hasta que el servidor pierda el acceso a la base de datos.

      Como sistema escrito, GraphQL puede verificar todo lo que el cliente solicite y reciba en una operación conocida como validación de esquema. Puede ver esto en acción realizando una “Query” con un campo no existente.

      Diríjase a http://localhost:8000/graphql​​​ en su navegador una vez más y ejecute la siguiente “Query” en la interfaz GraphiQL con el campo iDontExist:

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

      Ya que no hay un campo iDontExist definido en su “Query”, GraphQL muestra un mensaje de error:

      Output

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

      Esto es importante porque, en el sistema escrito de GraphQL, el objetivo es enviar y recibir solo la información ya definida en el esquema.

      La aplicación actual acepta cualquier cadena arbitraria en el campo full_url. El problema es que si alguien envía una URL mal construida, usted no redirigiría al usuario a ningún sitio al probar la información almacenada. En este caso, necesita verificar si la full_url está bien formateada antes de guardarla en la base de datos, y, si hay cualquier error, elevar la excepción GraphQLError con un mensaje personalizado.

      Vamos a implementar esta funcionalidad en dos pasos. Primero, abra el archivo shortener/models.py:

      Añada las líneas resaltadas en la sección import:

      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
      ...
      

      El URLValidator es un ayudante de Django para validar una cadena URL y Graphene utiliza GraphQLError para elevar excepciones con un mensaje personalizado.

      A continuación, asegúrese de validar la URL recibida por el usuario antes de guardarla en la base de datos. Habilite esta operación añadiendo el código resaltado en el archivo 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)
      

      Primero, este código inicia el URLValidator en la variable validate. Dentro del bloque try/except, validate() la URL recibida y eleve un GraphQLError con el mensaje personalizado invalid url si se produjo algún error.

      El archivo completo shorterner/models.py se muestra aquí:

      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)
      

      Guarde y cierre el archivo. Si no está ejecutando el servidor local, inícielo con el comando python manage.py runserver.

      A continuación, pruebe su nueva gestión de errores en http://localhost:8000/graphql. Intente crear una nueva URL con una full_url no válida en la interfaz GraphiQL:

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

      Cuando envíe una URL no válida, su excepción se elevará con el mensaje personalizado:

      Output

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

      Si mira en su terminal donde está en ejecución el comando python manage.py runserver, aparecerá un error:

      Output

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

      Un extremo de GraphQL siempre generará un error con un código de estado HTTP 200, lo que normalmente significa que el resultado es correcto. Recuerde que, aunque GraphQL se construye sobre HTTP, no utiliza los conceptos de los códigos de estado HTTP o los métodos HTTP como REST hace.

      Con el manejo de errores implementado, ahora puede implementar un mecanismo para filtrar sus “Query”, minimizando la información devuelta por el servidor.

      Paso 7: Implementar filtros

      Imagine que ha comenzado a usar el acortador de URL para añadir sus propios enlaces. Después de un tiempo, habrá tantas entradas que será difícil encontrar la correcta. Puede resolver este problema usando filtros.

      El filtrado es un concepto común en las API de REST; normalmente, se anexa a la URL un parámetro Query con un campo y un valor. A modo de ejemplo, para filtrar todos los usuarios llamados jojo, podría usar GET /api/users?name=jojo.

      En GraphQL, usará argumentos “Query” como filtros. Crean una interfaz perfecta y limpia.

      Puede resolver el problema “hard to find a URL” permitiendo que el cliente filtre las URL por nombre con el campo full_url. Para implementar eso, abra el archivo shortener/schema.py en su editor favorito.

      Primero, importe el método Q en la línea resaltada:

      shorty/shortener/schema.py

      import graphene
      from graphene_django import DjangoObjectType
      from django.db.models import Q
      
      from .models import URL
      ...
      

      Esto se usará para filtrar la consulta de su base de datos.

      A continuación, reescriba toda la clase Query con el siguiente contenido:

      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
      ...
      

      Las modificaciones que está realizando son:

      • Añadir el parámetro de filtro url dentro de la variable urls y el método resolve_url
      • Dentro de resolve_urls, si se da un parámetro llamado url, filtrar los resultados de la base de datos para devolver solo las URLs que contienen el valor dado, usando el método Q(full_url__icontains=url).

      El archivo completo shorterner/schema.py se muestra aquí:

      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()
      

      Guarde y cierre el archivo. Si no ejecuta el servidor local, inícielo con python manage.py runserver.

      Pruebe sus últimos cambios en http://localhost:8000/graphql. En la interfaz GraphiQL, escriba la siguiente instrucción. Filtrará todas las URL con la palabra community:

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

      El resultado es solo una entrada, ya que acaba de añadir una URL con la cadena community en ella. Si añadió más URL antes, el resultado puede variar.

      Output

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

      Ahora, tiene la capacidad de buscar en sus URLs. Sin embargo, con demasiados enlaces, sus clientes pueden quejarse de que la lista de URLs devuelve más datos de los que la aplicación puede gestionar. Para resolver esto, implementará la paginación.

      Paso 8 – Implementar la paginación

      Los clientes que usen su backend pueden quejarse de que el tiempo de respuesta es demasiado largo o que el tamaño es demasiado grande si hay demasiadas entradas de URL. Incluso puede ser difícil para su base de datos reunir un conjunto tan enorme de información. Para resolver este problema, puede permitir que el cliente especifique cuántos elementos quiere dentro de cada solicitud usando una técnica llamada paginación.

      No existe una forma predeterminada de implementar esta función. Incluso en las API de REST puede verlo en los encabezados HTTP o en los parámetros de consulta, con diferentes nombres y comportamientos.

      En esta aplicación, implementará la paginación habilitando dos argumentos más en la “Query” de URL: first y skip first seleccionará el primer número de elementos variables y skip especificará cuántos elementos deben omitirse desde el principio. Por ejemplo, si usa first == 10 y skip == 5 se obtendrán las primeras 10 URL, pero se omitirán 5, con lo cual se mostrarán solo las 5 restantes.

      Implementar esta solución es similar a añadir un filtro.

      Abra el archivo shortener/schema.py:

      En el archivo, cambie la clase Query añadiendo los dos nuevos parámetros en la variable urls y el método resolve_urls, resaltados en el siguiente código:

      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 utiliza los parámetros first y skip recién creados dentro del método resolve_urls para filtrar la consulta de la base de datos.

      Guarde y cierre el archivo. Si no está ejecutando el servidor local, inícielo con python manage.py runserver.

      Para probar la paginación, emita la siguiente “Query” en la interfaz GraphiQL en http://localhost:8000/graphql:

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

      Su acortador de URL mostrará la segunda URL creada en su base de datos:

      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" } ] } }

      Esto muestra que la función de paginación funciona. Puede experimentar añadiendo más URLs y probando diferentes conjuntos de first y skip.

      Conclusión

      Todo el ecosistema de GraphQL crece día a día y una comunidad activa lo respalda. Empresas como GitHub y Facebook han demostrado que está listo para la producción, y ahora puede aplicar esta tecnología a sus propios proyectos.

      En este tutorial, creó un acortador de URL usando GraphQL, Python y Django y conceptos como “Query” y “Mutation”. Pero sobre todo, ahora comprende cómo depender de estas tecnologías para crear aplicaciones web usando el marco web Django.

      Puede explorar más sobre GraphQL y las herramientas usadas aquí en el sitio web de GraphQL y en los sitios web de documentación de Graphene. Además, DigitalOcean tiene tutoriales adicionales para Python y Django que puede usar si desea aprender más sobre esto.



      Source link

      How To Create a URL Shortener with Django and GraphQL


      The author selected Girls Who Code to receive a donation as part of the Write for DOnations program.

      Introduction

      GraphQL is an API standard created and open-sourced by Facebook as an alternative to REST APIs. As opposed to REST APIs, GraphQL uses a typed system to define its data structure, where all the information sent and received must be compliant to a pre-defined schema. It also exposes a single endpoint for all communication instead of multiple URLs for different resources and solves the overfetching issue by returning only the data asked for by the client, thereby generating smaller and more concise responses.

      In this tutorial you will create a backend for a URL shortener—a service that takes any URL and generates a shorter, more readable version—while diving into GraphQL concepts, like queries and mutations, and tools, like the GraphiQL interface. You may already have used such services before, like bit.ly.

      Since GraphQL is a language agnostic technology, it is implemented on top of various languages and frameworks. Here, you will use the general purpose Python programming language, the Django web framework, and the Graphene-Django library as the GraphQL Python implementation with specific integrations for Django.

      Prerequisites

      Step 1 — Setting Up the Django Project

      In this step, you will be installing all the necessary tools for the application and setting up your Django project.

      Once you have created your project directory and started your virtual environment, as covered in the prerequisites, install the necessary packages using pip, the Python package manager. This tutorial will install Django version 2.1.7 and Graphene-Django version 2.2.0 or higher:

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

      You now have all the tools needed in your tool belt. Next, you will create a Django project using the django-admin command. A project is the default Django boilerplate—a set of folders and files with everything necessary to start the development of a web application. In this case, you will call your project shorty and create it inside your current folder by specifying the . at the end:

      • django-admin startproject shorty .

      After creating your project, you will run the Django migrations. These files contain Python code generated by Django and are responsible for changing the application’s structure according to the Django models. Changes might include the creation of a table, for example. By default, Django comes with its own set of migrations responsible for subsystems like Django Authentication, so it is necessary to execute them with the following command:

      This command uses the Python interpreter to invoke a Django script called manage.py, responsible for managing different aspects of your project, like creating apps or running migrations.

      This will give output similar to the following:

      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

      Once Django’s database is ready to go, start its local development server:

      • python manage.py runserver

      This will give:

      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.

      This command will take away the prompt in your terminal and start the server.

      Visit the http://127.0.0.1:8000 page in your local browser. You will see this page:

      Django local server front page

      To stop the server and return to your terminal, press CTRL+C. Whenever you need to access the browser, make sure the preceding command is running.

      Next, you will finish this step by enabling the Django-Graphene library in the project. Django has the concept of app, a web application with a specific responsibility. A project is composed of one or multiple apps. For now, open the shorty/settings.py file in your text editor of choice. This tutorial will be using vim:

      The settings.py file manages all the settings in your project. Inside it, search for the INSTALLED_APPS entry and add the 'graphene_django' line:

      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',
      ]
      ...
      

      This addition tells Django that you will be using an app called graphene_django, which you installed in Step 1.

      At the bottom of the file, add the following variable:

      shorty/shorty/settings.py

      ...
      GRAPHENE = {
          'SCHEMA': 'shorty.schema.schema',
      }
      

      This last variable points to your main Schema, which you will create later. In GraphQL, a Schema contains all the object types, such as Resources, Queries, and Mutations. Think of it as documentation representing all the data and functionality available in your system.

      After the modifications, save and close the file.

      Now you have configured the Django project. In the next step, you will create a Django app and its Models.

      Step 2 — Setting Up a Django App and Models

      A Django platform is usually composed of one project and many applications or apps. An app describes a set of features inside a project, and, if well-designed, can be reused across Django projects.

      In this step, you will create an app called shortener, responsible for the actual URL shortening feature. To create its basic skeleton, type the next command in your terminal:

      • python manage.py startapp shortener

      Here you used the parameters startapp app_name, instructing manage.py to create an app named shortener.

      To finish the app creation, open the shorty/settings.py file

      Add the app’s name to the same INSTALLED_APPS entry you modified before:

      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',
      ]
      ...
      

      Save and close the file.

      With your shortener added to shorty/settings.py, you can move on to creating the models for your project. Models are one of the key features in Django. They are used to represent a database in a “Pythonic” way, allowing you to manage, query, and store data using Python code.

      Before opening the models.py file for changes, this tutorial will give an overview of the changes you will make.

      Your model file—shortener/models.py—will contain the following content once you have replaced the existing code:

      shorty/shortener/models.py

      from hashlib import md5
      
      from django.db import models
      

      Here you will import the required packages needed by your code. You will add the line from hashlib import md5 at the top to import the Python standard library that will be used to create a hash of the URL. The from django.db import models line is a Django helper for creating models.

      Warning: This tutorial refers to hash as the result of a function that takes an input and always returns the same output. This tutorial will be using the MD5 hash function for demonstration purposes.

      Note that MD5 has collision issues and should be avoided in production.

      Next, you will add a Model named URL with the following fields:

      • full_url: the URL to be shortened.
      • url_hash: a short hash representing the full URL.
      • clicks: how many times the short URL was accessed.
      • created_at: the date and time at which the URL was created.

      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)
      

      You will generate the url_hash by applying the MD5 hash algorithm to the full_url field and using just the first 10 characters returned during the Model’s save() method, executed every time Django saves an entry to the database. Additionally, URL shorteners usually track how many times a link was clicked. You will achieve this by calling the method clicked() when the URL is visited by a user.

      The operations mentioned will be added inside your URL model with this code:

      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)
      

      Now that you’ve reviewed the code, open the shortener/models.py file:

      Replace the code with the following content:

      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)
      

      Be sure to save and close the file.

      To apply these changes in the database, you will need to create the migrations by running the following command:

      • python manage.py makemigrations

      This will give you the following output:

      Output

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

      Then execute the migrations:

      You will see the following output in your terminal:

      Output

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

      Now that you’ve set up the models, in the next step you will create the GraphQL endpoint and a Query.

      Step 3 — Creating Queries

      The REST architecture exposes different resources in different endpoints, each one containing a well-defined data structure. For example, you may fetch a users’ list at /api/users, always expecting the same fields. GraphQL, on the other hand, has a single endpoint for all interactions, and uses Queries to access data. The main—and most valuable—difference is that you can use a Query to retrieve all your users within a single request.

      Start by creating a Query to fetch all URLs. You will need a couple of things:

      • A URL type, linked to your previously defined model.
      • A Query statement named urls.
      • A method to resolve your Query, meaning to fetch all URLs from the database and return them to the client.

      Create a new file called shortener/schema.py:

      Start by adding the Python import statements:

      shorty/shortener/schema.py

      import graphene
      from graphene_django import DjangoObjectType
      
      from .models import URL
      

      The first line imports the main graphene library, which contains the base GraphQL types, like List. The DjangoObjectType is a helper to create a Schema definition from any Django model, and the third line imports your previously create URL model.

      After that, create a new GraphQL type for the URL model by adding the following lines:

      shorty/shortener/schema.py

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

      Finally, add these lines to create a Query type for the URL model:

      shorty/shortener/schema.py

      ...
      class Query(graphene.ObjectType):
          urls = graphene.List(URLType)
      
          def resolve_urls(self, info, **kwargs):
              return URL.objects.all()
      

      This code creates a Query class with one field named urls, which is a list of the previously defined URLType. When resolving the Query through the resolve_urls method, you return all the URLs stored in the database.

      The full shortener/schema.py file is shown here:

      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()
      

      Save and close the file.

      All the Queries must now be added to the main Schema. Think of it as a holder for all your resources.

      Create a new file in the shorty/schema.py path and open it with your editor:

      Import the following Python packages by adding the following lines. The first one, as already mentioned, contains the base GraphQL types. The second line imports the previously created Schema file.

      shorty/shorty/schema.py

      import graphene
      
      import shortener.schema
      

      Next, add the main Query class. It will hold, via inheritance, all the Queries and future operations created:

      shorty/shorty/schema.py

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

      Lastly, create the schema variable:

      shorty/shorty/schema.py

      ...
      schema = graphene.Schema(query=Query)
      

      The SCHEMA setting you defined in Step 2 points to the schema variable you’ve just created.

      The full shorty/schema.py file is shown here:

      shorty/shorty/schema.py

      import graphene
      
      import shortener.schema
      
      
      class Query(shortener.schema.Query, graphene.ObjectType):
          pass
      
      schema = graphene.Schema(query=Query)
      

      Save and close the file.

      Next, enable the GraphQL endpoint and the GraphiQL interface, which is a graphical web interface used to interact with the GraphQL system.

      Open the shorty/urls.py file:

      For learning purposes, delete the file contents and save it, so that you can start from scratch.

      The first lines you will add are Python import statements:

      shorty/shorty/urls.py

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

      The path function is used by Django to create an accessible URL for the GraphiQL interface. Following it, you import the csrf_exempt, which allows clients to send data to the server. A complete explanation can be found in the Graphene Documentation. In the last line, you imported the actual code responsible for the interface via GraphQLView.

      Next, create a variable named urlpatterns.

      shorty/shorty/urls.py

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

      This will stitch together all the code necessary to make the GraphiQL interface available in the graphql/ path:

      The full shortener/urls.py file is shown here:

      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))),
      ]
      

      Save the file and close it.

      Back in the terminal, run the python manage.py runserver command (if not running already):

      • python manage.py runserver

      Open your web browser at the http://localhost:8000/graphql address. You will be presented with this screen:

      GraphiQL interface

      The GraphiQL is an interface where you can run GraphQL statements and see the results. One feature is the Docs section on the top right. Since everything in GraphQL is typed, you get free documentation about all your Types, Queries, Mutations, etc.

      After exploring the page, insert your first Query on the main text area:

      query {
        urls {
          id
          fullUrl
          urlHash
          clicks
          createdAt
        }
      }
      

      This content shows how a GraphQL Query is structured: First, you use the keyword query to tell the server that you only want some data back. Next, you use the urls field defined in the shortener/schema.py file inside the Query class. From that, you explicitly request all the fields defined in the URL model using camel case-style, which is the default for GraphQL.

      Now, click on the play arrow button in the top left.

      You will receive the following response, stating that you still have no URLs:

      Output

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

      This shows that GraphQL is working. In your terminal, press CTRL+C to stop your server.

      You have accomplished a lot in this step, creating the GraphQL endpoint, making a Query to fetch all URLs, and enabling the GraphiQL interface. Now, you will create Mutations to change the database.

      Step 4 — Creating Mutations

      The majority of applications have a way to change the database state by adding, updating, or deleting data. In GraphQL, these operations are called Mutations. They look like Queries but use arguments to send data to the server.

      To create your first Mutation, open shortener/schema.py:

      At the end of the file, start by adding a new class named CreateURL:

      shorty/shortener/schema.py

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

      This class inherits the graphene.Mutation helper to have the capabilities of a GraphQL Mutation. It also has a property name url, defining the content returned by the server after the Mutation is completed. In this case, it will be the URLType data structure.

      Next, add a subclass named Arguments to the already defined class:

      shorty/shortener/schema.py

      ...
          class Arguments:
              full_url = graphene.String()
      

      This defines what data will be accepted by the server. Here, you are expecting a parameter named full_url with a String content:

      Now add the following lines to create the mutate method:

      shorty/shortener/schema.py

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

      This mutate method does a lot of the work by receiving the data from the client and saving it to the database. In the end, it returns the class itself containing the newly created item.

      Lastly, create a Mutation class to hold all the Mutations for your app by adding these lines:

      shorty/shortener/schema.py

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

      So far, you will only have one mutation named create_url.

      The full shortener/schema.py file is shown here:

      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()
      

      Close and save the file.

      To finish adding the Mutation, change the shorty/schema.py file:

      Alter the file to include the following highlighted code:

      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)
      

      Save and close the file. If you are not running the local server, start it:

      • python manage.py runserver

      Navigate to http://localhost:8000/graphql in your web browser. Execute your first Mutation in the GraphiQL web interface by running the following statement:

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

      You composed the Mutation with the createURL name, the fullUrl argument, and the data you want in the response defined inside the url field.

      The output will contain the URL information you just created inside the GraphQL data field, as shown here:

      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" } } } }

      With that, a URL was added to the database with its hashed version, as you can see in the urlHash field. Try running the Query you created in the last Step to see its result:

      query {
        urls {
          id
          fullUrl
          urlHash
          clicks
          createdAt
        }
      }
      

      The output will show the stored URL:

      Output

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

      You can also try executing the same Query, but only asking for the fields you want.

      Next, try it one more time with a different URL:

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

      The output will be:

      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" } } } }

      The system is now able to create short URLs and list them. In the next step, you will enable users to access a URL by its short version, redirecting them to the correct page.

      Step 5 — Creating the Access Endpoint

      In this step, you will use Django Views—a method that takes a request and returns a response—to redirect anyone accessing the http://localhost:8000/url_hash endpoint to its full URL.

      Open the shortener/views.py file with your editor:

      To start, import two packages by replacing the contents with the following lines:

      shorty/shortener/views.py

      from django.shortcuts import get_object_or_404, redirect
      
      from .models import URL
      

      These will be explained more thoroughly later on.

      Next, you will create a Django View named root. Add this code snippet responsible for the View at the end of your file:

      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)
      

      This receives an argument called url_hash from the URL requested by a user. Inside the function, the first line tries to get the URL from the database using the url_hash argument. If not found, it returns the HTTP 404 error to the client, which means that the resource is missing. Afterwards, it increments the clicked property of the URL entry, making sure to track how many times the URL is accessed. At the end, it redirects the client to the requested URL.

      The full shortener/views.py file is shown here:

      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)
      

      Save and close the file.

      Next, open shorty/urls.py:

      Add the following highlighted code to enable the root View.

      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'),
      ]
      

      The root View will be accessible in the / path of your server, accepting a url_hash as a string parameter.

      Save and close the file. If you are not running the local server, start it by executing the python manage.py runserver command.

      To test your new addition, open your web browser and access the http://localhost:8000/077880af78 URL. Note that the last part of the URL is the hash created by the Mutation from Step 5. You will be redirected to the hash’s URL page, in this case, the DigitalOcean Community website.

      Now that you have the URL redirection working, you will make the application safer by implementing error handling when the Mutation is executed.

      Step 6 — Implementing Error Handling

      Handling errors is a best practice in all applications, since developers don’t usually control what will be sent to the server. In this case, you can try to foresee failures and minimize their impacts. In a complex system such as GraphQL, a lot of things might go wrong, from the client asking for the wrong data to the server losing access to the database.

      As a typed system, GraphQL can verify everything the client asks for and receives in an operation called Schema Validation. You can see this in action by making a Query with a non-existing field.

      Navigate to http://localhost:8000/graphql in your browser once more, and execute the next Query within the GraphiQL interface, with the iDontExist field:

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

      Since there is no iDontExist field defined in your Query, GraphQL returns an error message:

      Output

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

      This is important because, in the GraphQL typed system, the aim is to send and receive just the information already defined in the schema.

      The current application accepts any arbitrary string in the full_url field. The problem is that if someone sends a poorly constructed URL, you would be redirecting the user to nowhere when trying the stored information. In this case, you need to verify if the full_url is well formatted before saving it to the database, and, if there’s any error, raise the GraphQLError exception with a custom message.

      Let’s implement this functionality in two steps. First, open the shortener/models.py file:

      Add the highlighted lines in the import section:

      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
      ...
      

      The URLValidator is a Django helper to validate a URL String and the GraphQLError is used by Graphene to raise exceptions with a custom message.

      Next, make sure to validate the URL received by the user before saving it to the database. Enable this operation by adding the highlighted code in the shortener/models.py file:

      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)
      

      First, this code instantiates the URLValidator in the validate variable. Inside the try/except block, you validate() the URL received and raise a GraphQLError with the invalid url custom message if something went wrong.

      The full shortener/models.py file is shown here:

      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)
      

      Save and close the file. If you are not running the local server, start it with the python manage.py runserver command.

      Next, test your new error handling at http://localhost:8000/graphql. Try to create a new URL with an invalid full_url in the GraphiQL interface:

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

      When sending an invalid URL, your exception will be raised with the custom message:

      Output

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

      If you look in your terminal where the python manage.py runserver command is running, an error will appear:

      Output

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

      A GraphQL endpoint will always fail with a HTTP 200 status code, which usually signifies success. Remember that, even though GraphQL is built on top of HTTP, it doesn’t use the concepts of HTTP status codes or HTTP methods as REST does.

      With the error handling implemented, you can now put in place a mechanism to filter your Queries, minimizing the information returned by the server.

      Step 7 — Implementing Filters

      Imagine you’ve started using the URL shortener to add your own links. After a while, there will be so many entries that finding the right one will become difficult. You can solve this issue using filters.

      Filtering is a common concept in REST APIs, where usually a Query Parameter with a field and value is appended to the URL. As an example, to filter all the Users named jojo, you could use GET /api/users?name=jojo.

      In GraphQL you will use Query Arguments as filters. They create a nice and clean interface.

      You can solve the “hard to find a URL” issue by allowing the client to filter URLs by name using the full_url field. To implement that, open the shortener/schema.py file in your favorite editor.

      First, import the Q method in the highlighted line:

      shorty/shortener/schema.py

      import graphene
      from graphene_django import DjangoObjectType
      from django.db.models import Q
      
      from .models import URL
      ...
      

      This will be used to filter your database query.

      Next, rewrite the whole Query class with the following content:

      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
      ...
      

      The modifications you are making are:

      • Adding the url filter parameter inside the urls variable and resolve_url method.
      • Inside the resolve_urls, if a parameter named url is given, filtering the database results to return only URLs that contain the value given, using the Q(full_url__icontains=url) method.

      The full shortener/schema.py file is shown here:

      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() 
      

      Save and close the file. If you are not running the local server, start it with python manage.py runserver.

      Test your latest changes at http://localhost:8000/graphql. In the GraphiQL interface, write the following statement. It will filter all the URLs with the word community:

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

      The output is only one entry since you just added one URL with the community string in it. If you added more URLs before, your output may vary.

      Output

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

      Now you have the ability to search through your URLs. However, with too many links, your clients might complain the URL list is returning more data than their apps can handle. To solve this, you will implement pagination.

      Clients using your backend might complain that the response time is taking too long or that its size is too big if there are too many URL entries. Even your database may struggle to put together a huge set of information. To solve this issue, you can allow the client to specify how many items it wants within each request using a technique called pagination.

      There’s no default way to implement this feature. Even in REST APIs, you might see it in HTTP headers or query parameters, with different names and behaviors.

      In this application, you will implement pagination by enabling two more arguments to the URLs Query: first and skip. first will select the first variable number of elements and skip will specify how many elements should be skipped from the beginning. For example, using first == 10 and skip == 5 gets the first 10 URLs, but skips 5 of them, returning just the remaining 5.

      Implementing this solution is similar to adding a filter.

      Open the shortener/schema.py file:

      In the file, change the Query class by adding the two new parameters into the urls variable and resolve_urls method, highlighted in the following code:

      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
      ...
      

      This code uses the newly created first and skip parameters inside the resolve_urls method to filter the database query.

      Save and close the file. If you are not running the local server, start it with python manage.py runserver.

      To test the pagination, issue the following Query in the GraphiQL interface at http://localhost:8000/graphql:

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

      Your URL shortener will return the second URL created in your database:

      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" } ] } }

      This shows that the pagination feature works. Feel free to play around by adding more URLs and testing different sets of first and skip.

      Conclusion

      The whole GraphQL ecosystem is growing every day, with an active community behind it. It has been proven production-ready by companies like GitHub and Facebook, and now you can apply this technology to your own projects.

      In this tutorial you created a URL shortener service using GraphQL, Python, and Django, using concepts like Queries and Mutations. But more than that, you now understand how to rely on these technologies to build web applications using the Django web framework.

      You can explore more about GraphQL and the tools used here in the GraphQL website and the Graphene documentation websites. Also, DigitalOcean has additional tutorials for Python and Django that you can use if you’d like to learn more about either.



      Source link