serverless
2 || 3
Stands up an AWS Lambda that acts as a GraphQL endpoint.
> npm install
to download dependencies locally
> serverless deploy
uses .aws/credentials
profile of personal-dev
(specified in serverless.yml:provider:profile)
ℹ️ After a full deploy, you can use > npm run deploy-gql
for faster deploy of changes.
gql.js
utils.js
📂 Files for our GraphQL
implementation.
Entities define the objects
or types
that our endpoint knows how to work with.
Every file in this folder is aggregated to build the total schema definition for our endpoint.
utils.gql.readSchema
(here) takes care of the aggregation.
📄 _Query.gql
queries
supported by our endpoint.
type Query {
pokemon(id: String!): Pokemon
allPokemon(page: Int, itemsPerPage: Int): PokemonPage
test(message: String!): TestObject
testNoParam: TestObject
}
I create two sample queries:
test(message: String!): TestObject
- This query requires a
message
parameter, and the query response will be oftype TestObject
. TestObject
is defined in filetest.gql
.
- This query requires a
testNoParam: TestObject
- This query does not require any parameters. This query also returns a
TestObject
- This query does not require any parameters. This query also returns a
📄 TestObject.gql
defines one type
, with all-scalar types: Scalar-types
type TestObject {
total: Int!
caption: String!
prop1: String!
prop2: String!
prop3: String!
}
I created better sample queries:
pokemon(id: String!): Pokemon
- Returns pokemon information for the specified
id
.
- Returns pokemon information for the specified
allPokemon(page: Int, itemsPerPage: Int): PokemonPage
- Returns pokemon and shows a
paging
approach.
- Returns pokemon and shows a
📄 Types
for these queries can be found in Pokemon.gql and Paging.gql
Here is Paging.gql
type Paging {
total: Int!
page: Int!
totalPages: Int!
itemsPerPage: Int!
}
📄 _Mutation.gql
mutations
supported by our endpoint.
Mutations are like queries, but are classified here because they "mutate" data, as opposed to queries where they only access/read data.
This is only convention and nothing prevents you from updating data from a query... maybe like updating a last_accessed
field.
Same for mutations, if nothing is actually updated, there is no gql-police 🚓.
type Mutation {
test(message: String!): CanResponse
}
type CanResponse {
success: Boolean!
message: String!
status: String
warning: String
}
There is one sample mutation:
test(message: String!): CanResponse
- Requires one string parameter and returns a
CanResponse
object, defined in the same file.
- Requires one string parameter and returns a
ℹ️ Because every file in the entities
folder is merged together, you are free to create one file per type, or group related types into a file...
Resolvers are your logic. Here you can import
whatever necessary, even other resolvers.
📂 Query
For every query
you define in 📄 _Query.gql
you need to define its respective resolver
and put it in this folder. This is a .js
file with the query name.
📂 Mutation
For every mutation
you define in 📄 _Mutation.gql
you need to define its respective resolver
and put it in this folder. This is a .js
file with the mutation name.
I have some resolver templates in /g/resolvers/
:
- 📄
_empty_resolver_sample.js
- 📄
_empty_resolver_sync.js
- 📄
_empty_resolver_promise.js
Crux:
// import things here
var resolver = (obj, args, ctx, info) => {
/* return object or promise */
// access query arguments/parameters
let myParam = args.myParam;
let msg = myParam;
return {
success: true,
message: msg
};
// or
return new Promise ((resolve, reject) => {
resolve({
success: true,
message: msg
});
})
.catch((err) => {
//reject(err);
// or
resolve({
success: false,
message: err.message
});
});
};
module.exports = resolver;
This shows some possibilities, note the // or
where things are "reiterated"; alternatives are given.
The lambda function receives graphql queries (& mutations) and each has a designated resolver, which needs to return/resolve an object with the properties defined by the query/mutation response type.
axios.post(MY_ENDPOINT, {
query: 'query { testNoParam { caption total } }'
})
.then((results) => {
console.log(results.data);
});
Logs:
{
"data": {
"testNoParam": {
"caption": "Hello user :)",
"total": 4
}
}
}
The query
query { testNoParam { caption total } }
means execute query testNoParam and get back fields caption & total.
The result of testNoParam
is wrapped within a parent object data
.
Normally, in a non-GraphQL API, it would take a call for each query execution. GraphQL enables bundling multiple actions for the API to work on...
query {
testNoParam { caption }
test(message:"Two queries!") { caption }
}
returns
{
"data": {
"testNoParam": {
"caption": "Hello user :)"
},
"test": {
"caption": "Two queries!"
}
}
}
There is a Chrome and Edge browser extension that lets you interact with your lambda graphql easily, it is called Altair.
And it also exists for Firefox.
Now that you have your GraphQL browser extension ready, here is the endpoint URL to see it in action!
https://rj07ty7re4.execute-api.us-east-1.amazonaws.com/dev/gql
And here are some demo queries to try out:
Grab the second page of available pokemon
{
allPokemon(page:2) {
items {
name type1 type2 id stats { attack defense stamina }
}
paging { total page itemsPerPage totalPages }
}
}
Get some details about ivysaur
{
pokemon(id:"ivysaur") {
name type1 type2 names
stats { attack defense stamina }
family parentId
moves { quick eliteQuick charge eliteCharge }
evolutionBranch { evolution candyCost form }
}
}
Now a request to get details for 3 pokemon, using the same request (as opposed to needing 3 separate requests on a typical REST API)
{
poke1: pokemon(id:"ivysaur") {
name type1 type2 names
stats { attack defense stamina }
moves { quick eliteQuick charge eliteCharge }
}
poke2: pokemon(id:"omanyte") {
name type1 type2 names
stats { attack defense stamina }
moves { quick eliteQuick charge eliteCharge }
}
poke3: pokemon(id:"gastly") {
name type1 type2 names
stats { attack defense stamina }
moves { quick eliteQuick charge eliteCharge }
}
}
Notice how we named
each query so the resulting data
object can hold them as siblings, and we can access them in our code using those keys
.
A 📄 JSON file where you can manage environment variables or other things.
There is an example of grabbing an ES_DOMAIN
value from config.json
and applying it to the environment of the gql
lambda function in serverless.yml
21: ES_DOMAIN: ${file(./config.json):ES_DOMAIN}
axios
graphql
@graphql-tools/schema
nanoid
base-64