Skip to content

readonlychild/starter-lambda-graphql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Serverless GraphQL on AWS Lambda

serverless 2 || 3

Stands up an AWS Lambda that acts as a GraphQL endpoint.

Initial setup

> npm install

to download dependencies locally

Deploying

> serverless deploy

⚠️ Requires Serverless Framework
⚠️ Requires the aws cli and an AWS account.

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.

Structure

gql.js ▶️ entry point
utils.js ▶️ utility belt

/g

📂 Files for our GraphQL implementation.

entities

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 ▶️ holds 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 of type TestObject.
    • TestObject is defined in file test.gql.
  • testNoParam: TestObject
    • This query does not require any parameters. This query also returns a TestObject

📄 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.
  • allPokemon(page: Int, itemsPerPage: Int): PokemonPage
    • Returns pokemon and shows a paging approach.

📄 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 ▶️ holds 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.

ℹ️ 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

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.

Learn more

graphql.org/learn

Simplest Explanation

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.

Using the Endpoint from JAM

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.

Paradigm shift

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!"
    }
  }
}

ChromeiQL-1

Dev & Test

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.

Demo

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.

Other

config.json

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}

dependencies

axios ▶️ for travelling over http[s] to get things.
graphql ▶️ GraphQL magic.
@graphql-tools/schema ▶️ allows GraphQL Schema Language support.
nanoid ▶️ for when you need unique ids.
base-64 ▶️ for when you want to send a lot of data and you don't care to create a "complex" type tree.

About

quick start for a lambda that does graphql

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published