GraphQL API Tutorial with RESTHeart
🔧 Configuration
This tutorial guides you through implementing a GraphQL API using RESTHeart, following the structure and concepts from the GraphQL.org tutorial on Star Wars characters and episodes.
Dive into the world of GraphQL and RESTHeart, and may the force be with your data!
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 only requires defining a JSON configuration, known as a GraphQL App Definition, and storing 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 to use Docker, see the 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.
cURL
curl -i -X PUT [INSTANCE-URL]/star-wars-characters \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http PUT [INSTANCE-URL]/star-wars-characters Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/star-wars-characters', {
method: 'PUT',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Create the gql-apps collection. This is where the GraphQL App definition will be inserted.
cURL
curl -i -X PUT [INSTANCE-URL]/gql-apps \
-H "Authorization: Basic [BASIC-AUTH]"
HTTPie
http PUT [INSTANCE-URL]/gql-apps Authorization:"Basic [BASIC-AUTH]"
JavaScript
fetch('[INSTANCE-URL]/gql-apps', {
method: 'PUT',
headers: {
'Authorization': 'Basic [BASIC-AUTH]'
}
})
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
3 Insert data
Create documents in the star-wars-characters collection:
cURL
curl -i -X POST [INSTANCE-URL]/star-wars-characters?wm=upsert \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d '[
{"_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}
]'
HTTPie
http POST [INSTANCE-URL]/star-wars-characters wm==upsert \
Authorization:"Basic [BASIC-AUTH]" \
Content-Type:application/json \
<<< '[
{"_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}
]'
JavaScript
fetch('[INSTANCE-URL]/star-wars-characters?wm=upsert', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify([
{"_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}
])
})
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
4 Get the GraphQl App Definition
Download the GraphQL app definition json file.
curl -s https://restheart.org/docs/mongodb-graphql/graphql-app-definition.json --output graphql-app-definition.json
5 Insert the GraphQL App Definition
To insert the GraphQL app definition, run the following:
cURL
curl -i -X POST [INSTANCE-URL]/gql-apps?wm=upsert \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/json" \
-d @graphql-app-definition.json
HTTPie
http POST [INSTANCE-URL]/gql-apps wm==upsert \
Authorization:"Basic [BASIC-AUTH]" \
Content-Type:application/json \
< graphql-app-definition.json
JavaScript
fetch('[INSTANCE-URL]/gql-apps?wm=upsert', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/json'
},
body: JSON.stringify({
// GraphQL App Definition JSON content
// See the full definition below
})
})
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
Note
|
Request body omitted because is quite long. Find it in the The full GraphQL APP definition section. |
Execute queries
hero
cURL
curl -i -X POST [INSTANCE-URL]/graphql/star-wars \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/graphql" \
-d '{
hero(episode: JEDI) {
name
friends {
name
}
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
}'
HTTPie
echo '{
hero(episode: JEDI) {
name
friends {
name
}
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
}' | http POST [INSTANCE-URL]/graphql/star-wars \
Authorization:"Basic [BASIC-AUTH]" \
Content-Type:application/graphql
JavaScript
fetch('[INSTANCE-URL]/graphql/star-wars', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/graphql'
},
body: `{
hero(episode: JEDI) {
name
friends {
name
}
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
}`
})
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
],
"primaryFunction": "Astromech"
}
}
}
search
cURL
curl -i -X POST [INSTANCE-URL]/graphql/star-wars \
-H "Authorization: Basic [BASIC-AUTH]" \
-H "Content-Type: application/graphql" \
-d '{
search(text: "an") {
... on Character {
name
}
... on Starship {
name
length
}
}
}'
HTTPie
echo '{
search(text: "an") {
... on Character {
name
}
... on Starship {
name
length
}
}
}' | http POST [INSTANCE-URL]/graphql/star-wars \
Authorization:"Basic [BASIC-AUTH]" \
Content-Type:application/graphql
JavaScript
fetch('[INSTANCE-URL]/graphql/star-wars', {
method: 'POST',
headers: {
'Authorization': 'Basic [BASIC-AUTH]',
'Content-Type': 'application/graphql'
},
body: `{
search(text: "an") {
... on Character {
name
}
... on Starship {
name
length
}
}
}`
})
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
{
"data": {
"search": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
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" } } }
}
}
}