Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQL in Python

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Marcin Gębala Marcin Gębala
October 24, 2024
48

GraphQL in Python

GraphQL is establishing itself as a foundation of the modern web development stack, particularly where a dynamic, single-page UI is required. It unlocks many great benefits, some of which are:

- Fetching only the required data necessary to render particular views, while eliminating the need to call and combine data from multiple endpoints.
- Developer experience through incredible out-of-the-box tooling available - interactive API explorers or code generators for statically typed languages used in the frontend.
- Ability to combine various APIs under a single gateway with federations.

There are multiple libraries and approaches to build GraphQL APIs in Python. In this talk, we will look at two different approaches with popular libraries: schema-first approach with Ariadne (https://github.com/mirumee/ariadne) and code-first approach with Graphene (https://github.com/graphql-python/graphene). We'll take a look at the architecture and different aspects of a Python web app that serves a GraphQL API:

- Basics of GraphQL - the most essential benefits, how it differs from REST, the main concepts, and examples.
- Schema-first approach with Ariadne - how to design the schema and implement queries and mutations to interact with our data.
- Code-first approach with Graphene - how to represent types and operations with classes, examples of a large production web app built with Django and Graphene, mapping Django models to GraphQL types.
- Common web-app recipes - how to deal with authentication, permissions, or database performance in a GraphQL-first backend app.

Avatar for Marcin Gębala

Marcin Gębala

October 24, 2024
Tweet

Transcript

  1. About me » Principal Developer at Saleor Commerce (saleor.io) »

    Specialize in web development using Python, Django and GraphQL » Based in Wrocław, Poland ! Marcin Gębala - PyCon APAC 2024
  2. GraphQL GraphQL is a query language for APIs and a

    runtime for fulfilling those queries with your existing data. » Alternative to REST » Fetching only data that is needed » Combining resources in a single request » Strong typing » Backend agnostic » Open-source Marcin Gębala - PyCon APAC 2024
  3. Use-cases » Building APIs for web and mobile apps »

    Works great with single-page apps! » Apollo Client + React + Typescript » Code generation » Federations » Combining multiple services under one gateway » Communication between microservices - possible but not that common and practical Marcin Gębala - PyCon APAC 2024
  4. Components of a GraphQL API » Schema - defines the

    structure of the API » Resolvers - functions that return data for queries and mutations def resolve_user(obj, info, **kwargs): user_id = kwargs["id"] return db.get_user_by_id(user_id) schema { query: Query mutation: Mutation } type Query { users: [User!]! user(id: ID!): User posts: [Post!]! } type Mutation { createPost(input: PostData!): CreatePostResponse! } type User { id: ID! email: String! name: String } type Post { id: ID! user: User! content: String! createdAt: String! } input PostData { user: ID! content: String! } type CreatePostResponse { error: String post: Post } Marcin Gębala - PyCon APAC 2024
  5. Schema-first vs code-first Two approaches to building GraphQL APIs: -

    Schema-first - explicitly define the schema, then implement the resolvers to comply with the schema - Code-first - define the schema by writing classes/functions, then generate the schema from the code Marcin Gębala - PyCon APAC 2024
  6. Ariadne Library for building GraphQL servers in Python. » Schema-first

    » Lightweight and simple » Asynchronous » Supports GraphQL modules and generating Python API clients github.com/mirumee/ariadne Marcin Gębala - PyCon APAC 2024
  7. Schema definition from ariadne import make_executable_schema, MutationType, ObjectType, QueryType type_defs

    = """ type Query { user(id: ID!): User } type Mutation { createPost(userId: ID!, content: String!): Post! } type User { id: ID! email: String! name: String posts: [Post!]! } type Post { id: ID! user: User! content: String! createdAt: String! } """ mutation = MutationType() query = QueryType() post = ObjectType("Post") user = ObjectType("User") schema = make_executable_schema(type_defs, mutation, query, user, post) Marcin Gębala - PyCon APAC 2024
  8. Resolvers @query.field("user") def resolve_user(*_, **kwargs): user_id = kwargs["id"] return db.get_user_by_id(user_id)

    @mutation.field("createPost") def resolve_create_post(*_, userId, content): error = None post = None try: post = db.create_post(userId, content) except Exception as e: error = str(e) return post Marcin Gębala - PyCon APAC 2024
  9. Running the app Ariadne provides both ASGI and WSGI application

    that can be mounted in any ASGI-compatible (WSGI-compatible) framework. from ariadne.asgi import GraphQL from fastapi import FastAPI from .schema import schema app = FastAPI() app.mount("/graphql/", GraphQL(schema=schema)) Marcin Gębala - PyCon APAC 2024
  10. Graphene High-level framework for building GraphQL APIs in Python. »

    Code-first approach - generating GraphQL schema from Python classes » Rich ecosystem of libraries and integrations (Django, Flask, SQLAlchemy, Mongo) graphene-python.org github.com/graphql-python/graphene Marcin Gębala - PyCon APAC 2024
  11. Queries from graphene import ObjectType, Schema class Query(ObjectType): user =

    graphene.Field(User, id=graphene.ID(required=True)) users = graphene.List(types.User) @staticmethod def resolve_user(root, info, id): return models.User.objects.filter(id=id).first() @staticmethod def resolve_users(root, info): return models.User.objects.all() schema = Schema(query=Query) Marcin Gębala - PyCon APAC 2024
  12. Types from graphene import DjangoObjectType class User(DjangoObjectType): credit_card_number = graphene.Field(String,

    description="User's credit card number.") class Meta: description = "Represents a user." model = models.User only_fields = [ "created_at", "email", "profile_picture_url", ] @staticmethod def resolve_credit_card_number(root: models.User, *_): # Custom resolver to anonymize the credit card number. return anonymize_credit_card(root.credit_card) Marcin Gębala - PyCon APAC 2024
  13. Mutations class UserCreate(graphene.Mutation): user = graphene.Field(types.User) errors = graphene.List(Error, required=True,

    default_value=[]) class Arguments: input = UserCreateInput(required=True) @classmethod def mutate(cls, root, info, input): user = models.User(**input) try: user.full_clean() except ValidationError as e: errors = validation_error_to_error_type(e) return UserCreate(errors=errors) user.save() send_activation_email(user) return UserCreate(user=user) Marcin Gębala - PyCon APAC 2024
  14. Serving the schema Expose the API using Django views: from

    django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ path("graphql", GraphQLView.as_view()), ] Marcin Gębala - PyCon APAC 2024
  15. Authentication JSON Web Token authentication is provided by django-graphql-jwt package:

    mutation { tokenCreate(email: "[email protected]", password: "secret") { token refreshToken user { id email } } } Authenticating requests with the Authorization header: { "Authorization": "JWT jwt-token-value" } Marcin Gębala - PyCon APAC 2024
  16. Permissions django-graphql-jwt also provides decorators to restrict access to particular

    fields in the schema. # types.py from graphql_jwt.decorators import permission_required class User(DjangoObjectType): credit_card_number = graphene.Field(String, description="User's credit card number.") @permission_required("user.manage_users") def resolve_credit_card_number(root, info): return anonymize_credit_card(root.credit_card) Marcin Gębala - PyCon APAC 2024
  17. N+1 problem Without optimization, the following query could result in

    duplicated database queries: { posts { name user { email } } } SELECT * FROM "blog_post"; SELECT * FROM "user" WHERE "blog_post"."id" = 1; SELECT * FROM "user" WHERE "blog_post"."id" = 2; SELECT * FROM "user" WHERE "blog_post"."id" = 3; ... SELECT * FROM "user" WHERE "blog_post"."id" = N; Marcin Gębala - PyCon APAC 2024
  18. Data loaders Lazy loading of related objects using data loaders.

    class Post(DjangoObjectType): user = graphene.Field(User, description="Author of the post.") @staticmethod def resolve_user(root: models.Product, info, *_): return UserByIdLoader(info.context).load(root.user_id) from promise import Promise from promise.dataloader import DataLoader class UserByIdLoader(DataLoader): def batch_load_fn(self, user_ids): # in_bulk creates a dictionary of {id: <User>, ...} users = models.User.objects.in_bulk(keys) results = [users.get(user_id) for user_id in user_ids] return Promise.resolve(results) Marcin Gębala - PyCon APAC 2024
  19. Versioning » By design GraphQL APIs doesn't use versioning. »

    Evolve the schema by adding new fields, types and queries. » Use deprecation mechanism for backward-compatibility type Query { user(id: ID!): User @deprecated(reason: "Use `userById` instead.") userById(id: ID!): User } Marcin Gębala - PyCon APAC 2024
  20. Resources » Books: » "Learning GraphQL" by Eve Porcello, Alex

    Banks » "Production Ready GraphQL" by Marc-Andre Giroux » Example implementations: » Saleor GraphQL API: github.com/saleor/saleor » Ariadne demo: github.com/maarcingebala/ariadne-demo » Libraries: » Graphene: graphene-python.org » Ariadne: ariadnegraphql.org Marcin Gębala - PyCon APAC 2024