Edit Page

GraphQL API Tutorial with RESTHeart

This tutorial guides you through the process of implementing a GraphQL API using RESTHeart, following the structure and concepts presented in the GraphQL.org tutorial on Star Wars characters and episodes, demonstrating the implementation using RESTHeart.

Dive into the world of GraphQL and RESTHeart, and may the force be with your data!

Note
While this tutorial provides buttons for executing requests using "Rest Ninja," please be aware that it may not seamlessly function with Safari due to HTTPS requirements. For a smoother experience, we recommend using Chrome or Firefox browsers (refer to Configure TLS).

Create the Star Wars GraphQL API

Important
This tutorial requires RESTHeart v7.6.4+
Note
You won’t write a single line of code! The GraphQL API just requires to define a json configuration, known as GraphQL App Definition and store it in the special collection /gql-apps.

1 Start RESTHeart and MongoDB

The following one-liner uses docker compose to start both RESTHeart and MongoDb. If you prefer not using docker, please refer to setup section to install RESTHeart and MongoDB.

$ curl https://raw.githubusercontent.com/SoftInstigate/restheart/master/docker-compose.yml --output docker-compose.yml && docker compose up

2 Create collections

Create the star-wars-characters collection. This is where the data will be inserted.

PUT /star-wars-characters HTTP/1.1

Create the gql-apps collection. This is where the GraphQL App definition will be inserted.

PUT /gql-apps HTTP/1.1

3 Insert data

Create documents in the star-wars-characters collection:

POST /star-wars-characters?wm=upsert HTTP/1.1

Request body

[
    {"_id":1000,"name":"Luke Skywalker","friends":[1002,1003,2000,2001],"appearsIn":[4,5,6],"homePlanet":"Tatooine","heroOf":4},
    {"_id":1001,"name":"Darth Vader","friends":[1004],"appearsIn":[4,5,6],"homePlanet":"Tatooine","heroOf":5},
    {"_id":1002,"name":"Han Solo","friends":[1000,1003,2001],"appearsIn":[4,5,6],"homePlanet":"Corellia"},
    {"_id":1003,"name":"Leia Organa","friends":[1000,1002,2000,2001],"appearsIn":[4,5,6],"homePlanet":"Alderaan"},
    {"_id":1004,"name":"Wilhuff Tarkin","friends":[1001],"appearsIn":[4],"homePlanet":"Eriadu"},{"_id":2000,"name":"C-3PO","friends":[1000,1002,1003,2001],"appearsIn":[4,5,6],"primaryFunction":"Protocol"},
    {"_id":2001,"name":"R2-D2","friends":[1000,1002,1003],"appearsIn":[4,5,6],"primaryFunction":"Astromech","heroOf":6},
    {"_id":3000,"name":"TIE Advanced x1","length":9.2}
]

4 Insert the GraphQL App Definition

To insert the GraphQL app definition, run the following:

POST /gql-apps?wm=upsert HTTP/1.1
Note
Request body omitted because is quite long. Find it in the The full GraphQL APP definition section.
That’s it

The GraphQL API is ready to use!

Execute queries

hero

POST /graphql/star-wars HTTP/1.1

Request body

{
	hero(episode: JEDI) {
        name
        friends {
            name
        }
        ... on Droid {
            primaryFunction
        }
        ... on Human {
            homePlanet
        }
    }
}
Response
{
    "data": {
        "hero": {
            "name": "R2-D2",
            "friends": [
                {
                    "name": "Luke Skywalker"
                },
                {
                    "name": "Han Solo"
                },
                {
                    "name": "Leia Organa"
                }
            ],
            "primaryFunction": "Astromech"
        }
    }
}
Note
Please note that the response content-type is application/graphql-response+json. It is treated as a binary format by RestNinja, which results in a less user-friendly display. To circumvent this issue and view the response more effectively, you can opt to use HTTPie, a command-line HTTP client:
$ echo '{ hero(episode: JEDI) { name friends { name } ... on Droid { primaryFunction } ... on Human { homePlanet } } }' | http -a admin:secret :8080/graphql/star-wars Content-Type:application/graphql
POST /graphql/star-wars HTTP/1.1

Request body

{
	search(text: "an") {
        ... on Character {
            name
        }
        ... on Starship {
            name
            length
        }
    }
}
Response
{
    "data": {
        "search": [
            {
                "name": "Han Solo"
            },
            {
                "name": "Leia Organa"
            },
            {
                "name": "TIE Advanced x1",
                "length": 9.2
            }
        ]
    }
}
Note
Please note that the response content-type is application/graphql-response+json. It is treated as a binary format by RestNinja, which results in a less user-friendly display. To circumvent this issue and view the response more effectively, you can opt to use HTTPie, a command-line HTTP client:
$ echo '{ search(text: "an") { ... on Character { name } ... on Starship { name length } } }' | http -a admin:secret :8080/graphql/star-wars Content-Type:application/graphql

Understanding the GraphQL App Definition

GraphQL types are connected to MongoDB data through mappings.

Note
If you don’t explicitly define a mapping for a field, RESTHeart will automatically map it to the MongoDB document field with the same name.

enum Episode

The GraphQL schema defines the enum Episode:

enum Episode { NEWHOPE EMPIRE JEDI }

The enum type requires a mapping unless the value in the database is identical to the string representation of the enum value. If you look at the data, the episode are stored with Int codes. Thus we need a mapping to link the Int codes to the enum values.

Tip
for more information on enum mappings see Enum mappings
{
    "mappings": {
        "Episode": { "NEWHOPE": 4, "EMPIRE": 5, "JEDI": 6 }
    }
}

union SearchResult

The GraphQL schema defines the union SearchResult:

union SearchResult = Human | Droid | Starship

The union requires a $typeResolver

Tip
for more information on union mappings see Union Mappings
{
    "mappings": {
        "SearchResult": {
            "$typeResolver": {
                "Human": "field-exists(homePlanet)",
                "Droid": "field-exists(primaryFunction)",
                "Starship": "field-exists(length)"
            }
        }
    }
}

interface Character

The GraphQL schema defines the interface Character:

interface Character {
  _id: Int!
  name: String!
  friends: [Character]!
  appearsIn: [Episode]!
}

The interface requires a $typeResolver

Tip
for more information on interface mappings see Interface Mappings
{
    "mappings": {
        "Character": {
            "$typeResolver": {
                "Human": "field-exists(homePlanet)",
                "Droid": "field-exists(primaryFunction)"
            }
        }
    }
}

object Starship

The GraphQL schema defines the object type Starship:

type Starship {
  _id: Int!
  name: String!
  length(unit: LengthUnit = METER): Float
}

No mapping is required since default field-to-field mappings are fine

Tip
for more information on field-to-field mappings see Field-to-field mapping

objects Human and Droid

The GraphQL schema defines the object types Human and Droids:

type Human implements Character {
  _id: Int!
  name: String!
  friends: [Character]!
  appearsIn: [Episode]!
  homePlanet: String!
}

type Droid implements Character {
  _id: Int!
  name: String!
  friends: [Character]!
  appearsIn: [Episode]!
  primaryFunction: String!
}

Those object types have the field friends in common, actually derived by the fact that they both implement the interface Character. This field requires a field-to-query mapping.

Tip
for more information on field-to-query mappings see Field-to-query Mapping
Tip
for more information on the $fk operator see Mapping Operators
{
    "mappings": {
        "Human": {
            "friends": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "_id": { "$in": { "$fk": "friends" } } }
            }
        },
        "Droid": {
            "friends": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "_id": { "$in": { "$fk": "friends" } } }
            }
        }
    }
}

Query

The GraphQL schema defines the queries hero and search

type Query {
  hero(episode: Episode!): Character
  search(text: String!): [SearchResult]
}

Queries always require mappings.

Tip
for more information on query mappings see Field-to-query Mapping
{
    "mappings": {
        "Query": {
            "hero": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "heroOf": { "$arg": "episode" } }
            },
            "search": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "name": { "$regex": { "$arg": "text" } , "$options": "i" } } }
        }
    }
}

The full GraphQL Schema

union SearchResult = Human | Droid | Starship

enum LengthUnit {
  METER
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

type Starship {
  _id: Int!
  name: String!
  length(unit: LengthUnit = METER): Float
}

interface Character {
  _id: Int!
  name: String!
  friends: [Character]!
  appearsIn: [Episode]!
}

type Human implements Character {
  _id: Int!
  name: String!
  friends: [Character]!
  appearsIn: [Episode]!
  homePlanet: String!
}

type Droid implements Character {
  _id: Int!
  name: String!
  friends: [Character]!
  appearsIn: [Episode]!
  primaryFunction: String!
}

type Query {
  hero(episode: Episode!): Character
  search(text: String!): [SearchResult]
}

The full GraphQL APP definition

{
    "_id": "star-wars",
    "descriptor": {
        "name": "star-wars",
        "description": "GraphQL application used in the Star Wars Tutorial",
        "enabled": true,
        "uri": "star-wars"
    },
    "schema": "union SearchResult = Human | Droid | Starship enum LengthUnit { METER } enum Episode { NEWHOPE EMPIRE JEDI } type Starship { _id: Int! name: String!  length(unit: LengthUnit = METER): Float } interface Character { _id: Int! name: String! friends: [Character]! appearsIn: [Episode]! } type Human implements Character { _id: Int! name: String! friends: [Character]! appearsIn: [Episode]! homePlanet: String! } type Droid implements Character { _id: Int! name: String! friends: [Character]! appearsIn: [Episode]! primaryFunction: String! } type Query { hero(episode: Episode!): Character search(text: String!): [SearchResult] }",
    "mappings": {
        "Episode": { "NEWHOPE": 4, "EMPIRE": 5, "JEDI": 6 },
        "SearchResult": {
            "$typeResolver": {
                "Human": "field-exists(homePlanet)",
                "Droid": "field-exists(primaryFunction)",
                "Starship": "field-exists(length)"
            }
        },
        "Character": {
            "$typeResolver": {
                "Human": "field-exists(homePlanet)",
                "Droid": "field-exists(primaryFunction)"
            }
        },
        "Human": {
            "friends": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "_id": { "$in": { "$fk": "friends"} } }
            }
        },
        "Droid": {
            "friends": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "_id": { "$in": { "$fk": "friends"} } }
            }
        },
        "Query": {
            "hero": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "heroOf": { "$arg": "episode" } }
            },
            "search": {
                "db": "restheart",
                "collection": "star-wars-characters",
                "find": { "name": { "$regex": { "$arg": "text" } , "$options": "i" } } }
        }
    }
}