Building a GraphQL API with Nitric

What we'll be doing

GraphQL APIs rely on only one HTTP endpoint, which means that you want it to be reliable, scalable, and performant. By using serverless compute such as Lambda, the GraphQL endpoint can be auto-scaling, whilst maintaining performance and reliability.

We'll be using Nitric to create a GraphQL API, that can be deployed to a cloud of your choice, gaining the benefits of serverless compute.

  1. Create the GraphQL Schema
  2. Write Resolvers
  3. Create handler for GraphQL requests
  4. Run locally for testing
  5. Deploy to a cloud of your choice

Video

Here's a video of this guide built with Node.js:

Serverless GraphQL on any Cloud

Prerequisites

Getting started

We'll start by creating a new project for our API.

nitric new my-profile-api "official/Python - Starter"

Next, open the project in your editor of choice.

cd my-profile-api

Make sure all dependencies are resolved:

Using Pipenv:

pipenv install --dev

The scaffolded project should have the following structure:

+--functions/
|  +-- hello.py
+--nitric.yaml
+--Pipfile
+--Pipfile.lock
+--README.md

You can test the project to verify everything is working as expected:

Start the Nitric server to emulate cloud services on your machine:

nitric start

Next, in a new terminal window, you can run your application:

pipenv run dev

If everything is working as expected you can now delete all files in the functions/ folder, we'll create new functions in this guide.

Build the GraphQL Schema

GraphQL requests are typesafe, and so they require a schema to be defined to validate queries.

Let's first add the Ariadne library

pipenv install ariadne

Create a new file named 'graphql.py' in the functions folder. We can then import our dependencies, and write out the schema.

from ariadne import MutationType, QueryType, gql, make_executable_schema, graphql
from uuid import uuid4

type_defs = gql("""
  type Profile {
    pid: String!
    name: String!
    age: Int!
    home: String!
  }

  type Message {
    msg: String!
  }

  input ProfileInput {
    name: String!
    age: Int!
    home: String!
  }

  type Query {
    getProfiles: [Profile]
    getProfile(pid: String!): Profile
  }

  type Mutation {
    createProfile(profile: ProfileInput!): Profile
    updateProfile(pid: String!, profile: ProfileInput!): Profile
  }
""")

Define a Collection

Lets define a collections resource for the resolvers get/set data from.

from nitric.resources import api, collection
from nitric.application import Nitric

profiles = collection('profiles').allow('reading','writing')

Create Resolvers

We'll need to map our resolvers to mutations or queries using Ariadne's QueryType or MutationType.

query = QueryType()
mutation = MutationType()

We can then use the collection within these functions. Each resolver will receive a parent and info arguments, as well as any query or mutation's arguments as keyword arguments.

An example of this is converting the GraphQL query function into Python:

updateProfile(pid: String!, profile: ProfileInput!): Profile
@mutation.field("updateProfile")
async def update_profiles(obj, info, pid, profile):

Create a profile

@mutation.field("createProfile")
async def resolve_create_profile(obj, info, profile):

    pid = str(uuid4())

    p = { 'pid': pid, 'name': profile['name'], 'age': profile['age'], 'home': profile['home'] }
    await profiles.doc(pid).set(p)

    return p

Update a profile

@mutation.field("updateProfile")
async def update_profiles(obj, info, pid, profile):

    try:
        await profiles.doc(pid).get()
    except:
        return { 'msg': f'Profile with id {pid} not found.'}

    p = { 'pid': pid, 'name': profile['name'], 'age': profile['age'], 'home': profile['home'] }
    await profiles.doc(pid).set(p)

    return p

Get all profiles

@query.field("getProfiles")
async def resolve_get_profiles(obj, info):

    results = await profiles.query().fetch()

    r = []
    for docs in results.documents:
        r.append(docs.content)

    return r

Get a profile by its ID

@query.field("getProfile")
async def resolve_get_profile(obj, info, pid):

    p = await profiles.doc(pid).get()
    return { 'pid': pid, 'name': p.content['name'], 'age': p.content['age'], 'home': p.content['home'] }

GraphQL Handler

We'll define an api to put our handler in. This api will only have one endpoint, which will handle all the requests.

First load the schema with our queries and mutations.

graph_api = api("public")
schema = make_executable_schema(type_defs, [query, mutation])

Then add the api handler.

@graph_api.post("/")
async def profile_handler(ctx: HttpContext) -> None:
    query = ctx.req.json

    success, result = await graphql(
        schema,
        query
    )

    ctx.res.status = 200 if success else 400
    ctx.res.body = result

Nitric.run()

Run it!

Now that you have an API defined with a handler for the GraphQL requests, it's time to test it out locally.

Start your Nitric server:

nitric start

Then test out your application with the following command in a new terminal:

pipenv run dev

Once it starts, the application will be able to receive requests via the API port.

Pressing ctrl + c will end the application.

GraphQL Queries

We can use cURL, postman or any other HTTP Client to test our application, however it's better if the client has GraphQL support.

Get all Profiles using cURL

curl --location -X POST \
  'http://localhost:4001' \
  --header 'Content-Type: application/json' \
  --data-raw '{"query":"query { getProfiles { pid name age home }}","variables":{}}'
{
  "data": {
    "getProfiles": [
      {
        "pid": "3f70ca58-25ed-4e88-8a45-eea1fbbb45d8",
        "name": "Tony Stark",
        "age": 53,
        "home": "Manhattan, New York City"
      },
      {
        "pid": "9c53bd95-199c-4151-a2a6-0da3ae24c29d",
        "name": "Peter Parker",
        "age": 22,
        "home": "Queens, New York City"
      },
      {
        "pid": "9ff191b0-0fbe-4e49-b944-85e79b5caa21",
        "name": "Steve Rogers",
        "age": 105,
        "home": "New York City"
      }
    ]
  }
}

Get a single profile

curl --location -X POST \
  'http://localhost:4001' \
  --header 'Content-Type: application/json' \
  --data-raw '{"query":"query { getProfile(pid: \"3f70ca58-25ed-4e88-8a45-eea1fbbb45d8\") { pid name age home }}","variables":{}}'

{
  "data": {
    "getProfile": {
      "pid": "3f70ca58-25ed-4e88-8a45-eea1fbbb45d8",
      "name": "Tony Stark",
      "age": 53,
      "home": "Manhattan, New York City"
    }
  }
}

Create a profile

curl --location -X POST \
  'http://localhost:4001' \
  --header 'Content-Type: application/json' \
  --data-raw '{"query":"mutation { createProfile(profile: { name: \"Tony Stark\", age: 53, home: \"Manhattan, New York City\" }){ pid name age home }}","variables":{}}'
{
  "data": {
    "getProfile": {
      "pid": "3f70ca58-25ed-4e88-8a45-eea1fbbb45d8",
      "name": "Tony Stark",
      "age": 53,
      "home": "Manhattan, New York City"
    }
  }
}

Update a profile

curl --location -X POST \
  'http://localhost:4001' \
  --header 'Content-Type: application/json' \
  --data-raw '{"query":"mutation { updateProfile(pid: \"3f70ca58-25ed-4e88-8a45-eea1fbbb45d8\",profile: { name: \"Peter Parker\", age: 22, home: \"Queens, New York City\" }){ pid name age home }}","variables":{}}'
{
  "data": {
    "getProfile": {
      "pid": "3f70ca58-25ed-4e88-8a45-eea1fbbb45d8",
      "name": "Peter Parker",
      "age": 22,
      "home": "Queens, New York City"
    }
  }
}

Deploy to the cloud

Setup your credentials and any other cloud specific configuration:

Create a stack - a collection of resources identified in your project which will be deployed.

nitric stack new
? What should we name this stack? dev
? Which provider do you want to deploy with? aws
? Which region should the stack deploy to? us-east-1

You can then deploy using the following command:

nitric up

To undeploy run the following command:

nitric down