Skip to main content

Deploying a Backend with a GraphQL API

In our previous tutorials,

We learnt how to manage data with ZefDB and perform some actions with ZefOp. Now that we have data stored in our database, we want to make it available to external applications via an API. In this tutorial, we will walkthrough how to set up a GraphQL server with minimal effort.

Importing Zef

Lets import the modules that will be used in this tutorial.

from zef.ops import *  
from zef.core import *
from zef.graphql import *
from rich.pretty import pprint

Declaring GraphQL Schema

We want to create a simple GraphQL API, with two endpoints(query type),

  • yo() - that returns a simple "Hello World!"
  • actors() - that returns a list of actors

The following code snippet represents a GraphQL schema, as per the official specs, which is a blueprint that defines the types of data that a GraphQL server can access:

type Query {  
yo(): String
actors(): [Actor]
}

type Actor {
id: ID
firstName: String
lastName: String
}

Generating Data Representation in Python

Let's declare a plain string variable in Python for the GraphQL Schema.

schema = """  
type Query {
yo(): String
actors(): [Actor]
}

type Actor {
id: ID
firstName: String
lastName: String
}
"""

Then, we will pass this as an input to a helped function generate_schema_dict to generate the representation of the schema in the form a Python dictionary:

schema_dict = parse_schema(schema)  
pprint(schema_dict, indent_guides=False)

which will give the following output:

{  
'GraphQLTypes': {
'Query': {
'yo': {'type': 'String', 'resolver': None},
'actors': {'type': '[Actor]', 'resolver': None}
},
'Actor': {
'id': {'type': 'ID', 'resolver': None},
'firstName': {'type': 'String', 'resolver': None},
'lastName': {'type': 'String', 'resolver': None}
}
}
}

Filling in Resolvers

Resolvers in GraphQL are functions that determine how to fetch and return data for each field requested in a GraphQL query. The GraphQL's Object Types fits nicely with Zef's Data model, hence, it is trivial to resolve the fields of the Actor by using the F operator:

{  
...
"Actor": {
"id": {"type": "ID", "resolver": uid},
"firstName": {"type": "String", "resolver": F.first_name},
"lastName": {"type": "String", "resolver": F.last_name},
}
}

For the actors Query type, let's try to write the a custom resolver that retrieves the all the actors in our database:

@func  
def get_actors(query_args, db):
res = (
db
| now
| all[ET.Actor]
)

return res

It's important to note that we don't use the collect ZefOp in the resolver. The collect operator is invoked at runtime when a user makes a query or mutation.

The final Python dictionary of the GraphQL schema will look like the following:

{  
'GraphQLTypes': {
'Query': {
'yo': {'type': 'String', 'resolver': lambda _ : "Hello World"},
'actors': {'type': '[Actor]', 'resolver': get_actors}
},
'Actor': {
'id': {'type': 'ID', 'resolver': uid},
'firstName': {'type': 'String', 'resolver': F.first_name},
'lastName': {'type': 'String', 'resolver': F.last_name}
}
}
}

Exploring our API

Now that we have fully defined our schema as a Python data structure, all we need to do is spin up the GraphQL server. Zef has its own way of handling side effects, called ZefFX, which includes a GraphQL server as one of the side effect that comes with the core library.

Run the following to start a GraphQL Playground with ZefFX:

my_playground = FX.GraphQL.StartPlayground(  
schema_dict = schema_dict,
db = db,
port = 5002,
) | run

To stop the GraphQL playground, run:

FX.GraphQL.StopPlayground(  
**my_playground
) | run

Hosting our API

my_server = FX.GraphQL.StartServer(  
schema_dict = schema_dict,
db = db,
port = 5002,
) | run

For this command, the GraphQL playground will be disabled as we don't want to expose that page in a production setting.

Recap

In this tutorial, we learned how to set up a simple GraphQL API with Zef. We explored how to write custom resolvers using ZefOps, and also briefly introduced the effect system for Zef called ZefFX.
Check this tutorial out, where we dive deeper into how to write complex queries and mutations with the same dataset.

One key advantage of using Zef is that it allows us to write both the database and backend in pure Python code. This makes it easier to maintain and update the codebase, and reduces the potential for errors that can arise from having separate codebases for different components of the system.