Skip to content

Instantly share code, notes, and snippets.

@Renddslow
Created February 28, 2019 16:55
Show Gist options
  • Save Renddslow/c0e9ef109d89bbd10a7399f328d98c71 to your computer and use it in GitHub Desktop.
Save Renddslow/c0e9ef109d89bbd10a7399f328d98c71 to your computer and use it in GitHub Desktop.

Intro

A key part of the Agility to the Web architecture is our middleware layer. Our middleware is build on top of a number of technologies including GraphQL, Golang, Node.js, and Docker. In order to get a high-level understanding of the full middleware architecture, we're going to follow a request to the middleware and make pitstops at points of interest along the way. There are a few terms I want to high-level before we get in. I assume most of you have heard of or used GraphQL at this point?

GraphQL is first and foremost a query language for APIs. Below that, they provide a system for interpreting and fulfilling those queries. Like any query language, GraphQL allows us to ask for what ever we need and return exactly that. In addition, it is able to manage multiple queries at a shot, dropping the number of requests we have to make from the frontend. In addition GraphQL provides a powerful type system which also helps to power its self-documentations.

We won't spend a ton of time on the ins-and-outs of GraphQL so encorage all of you to checkout graphql.org which provides documentation and tutorials. I will be sending out a follow up email with a list of links and resources and that will be included.

GraphiQL

Every request to the middleware starts from the frontend. For the purposes of this meeting we're going to follow along with phases because they're simple.

Demonstrate phases in AW

So we've created, updated, deleted, read, and seen a list of phases. Let's take a look at what's going on under the hood.

This is GraphiQL. It's a visual tool to help you write queries as well as inspect the schema.

Demonstrate phases in GraphiQL

Here in the left panel I have list of queries and mutations.

Queries are a type of call that are read-only. Nothing should ever change, or mutate, in the course of a query. Examples of queries are listing statuses, fetching a phase, fetching session information, and so on. Mutations on the other hand are requests where data is expected to be modified in some fashion. Examples of mutations include any CRUD operation, favoriting a viewer, and logging in or out of the application.

mutation CreatePhase($createPhase:ProjectPhaseCreateInput!) {
  createPhase(input:$createPhase) {
    phase {
      displaySequence
      id
      ownerID
      phaseTitle
      sequence
      phaseGUID
    }
  }
}

mutation UpdatePhase($updatePhase:ProjectPhaseUpdateInput!) {
  updatePhase(input:$updatePhase) {
    phase {
      displaySequence
      id
      ownerID
      phaseTitle
      sequence
      phaseGUID
    }
  }
}

mutation DeletePhase($deletePhase:ProjectPhaseDeleteInput!) {
  deletePhase(input:$deletePhase) {
    deletedProjectPhaseID
  }
}

query Phases {
  agilityProjects {
    phases(projectID:345) {
      displaySequence
      id
      ownerID
      phaseTitle
      sequence
      phaseGUID
    }
  }
}

query Phase {
  agilityProjects {
    phase(projectID:345, sequence:1) {
      displaySequence
      id
      ownerID
      phaseTitle
      sequence
      phaseGUID
    }
  }
}

Down here below the queries is a box for variables. In our case, we need to provide some information to our mutations in order to do the expected operations.

{
  "createPhase": {
    "projectID": 345,
    "clientMutationId": "c5042b66-9356-4867-9926-f10fd6ecdc09",
    "phaseDetails": {
      "phaseTitle": "Dry-Dock",
      "ownerID": "Brad"
    }
  },
  "updatePhase": {
    "projectID": 345,
    "clientMutationId": "c5042b66-9356-4867-9926-f10fd6ecdc09",
    "projectID": 345,
    "sequence": 1,
    "phaseDetails": {
      "phaseTitle": "Dry-Dock the Enterprise",
      "ownerID": "Brad"
    }
  },
  "deletePhase": {
    "projectID": 345,
    "sequence": 1,
    "deleteTasks": true,
    "clientMutationId": "c5042b66-9356-4867-9926-f10fd6ecdc09",
    "phaseGUID": ""
  }
}

Schemas and Types (pt. 1)

I want to make a quick pit-stop here to talk about schemas and types.

In GraphQL, the schema is the grand sum of all the types, queries, mutations, interfaces, nodes, and subscriptions. In GraphiQL you can see that in the right hand panel called Documentation Explorer.

So in the case of a phase we would find that in Query -> AgilityProjects -> Phase. Phase you'll see here shows what arguments the query expects then shows its output type. In our case we see that Phase outputs a ProjectPhase type. If we click on that, we can see every field that will be returned in that request.

One of the more powerful aspects of this is the idea that this type is shared across the whole schema. Viewer for instance could have a field called projectPhase which returns a ProjectPhase type. We can see that in action here with the tasks field.

We'll walk through how those types are built shortly. But for now, you can explore all the types, queries, and mutations for Agility to the Web at graphiql.dmsi.com. Also in your handout.

Basic Composition of a Middleware Service

As I mentioned earlier two of the technologies that we use to build the middleware are Golang and Node.js. Since we're following a request to the projects-api middleware service we'll mostly be in Go for the rest of this meeting.

Go is a compiled, typed language originally built by a team at Google. The low-level nature of the language allows for fast but readable code without having to worry about memory management. We're not going to spend a ton of time on the ins-and-outs of Go, but I will provide a few links in the handout to get up and running with Go.

Each of our go services start off in main.go. All of our main.go's look roughly the same with the notable differences of line 20 and 21. Here we define what the URI to each service will be. Typically this matches the repo name, in this case: projects-api. In Go, the main function in main.go will be one of the very first functions called. This method sets up a connection to Redis, sets up the HTTP server, and listens for requests made to "/projects-api". The listener is where all the magic happens. service.GraphqlHandler takes an argument of the given service's GraphQL schema and handles the request accordingly.

This leads us to schema.go. Most of our schema.go's look very similar, we build root query type and a root mutation type. Every time we add a new query or mutation for a service it must be added to its respective group. You'll see here under AgilityProjects the same list of queries that we saw in the GraphiQL documentation.

Each of these methods returns a GraphQL definition, that is a name, a description, the type that it will return, arguments required in order to get the information, and a resolver.

When the service has identified the query you're trying to call, it sends all of your arguments into the resolver. Inside the resolver we call a method called GetPhase then return the result of that method in the resolver.

  • Walk through request
    • Note the lack of need for "request" at the top level
  • Walk through the response
  • Walk through typing

Middleware on the Server

With that we've looked at one middleware service. But as of today we have six and a half. So how does that work?

In front of all our services we have a service called graphql-api. GraphQL API goes out and fetches the schema for all of our microservices and merges them together into one big schema. From there, it determines which service or services need to handle a given request and divvies them out to each one. We're not going to spend a lot of time on this because at this point it really is just tooling. Though there are two things I'll note. The first is inside the src/ folder there are a list of resourceUrls. This file is what controls which services we merge into our schema. The second is that we maintain our current version of the API inside of the meta field in the top-level query. This is driven by the version in our package.json.

The last thing I want to touch on is how we stand these up on our development server. All of our servers are containerized with Docker. We used a tool called docker compose to manage all of our containers on a given server.

Demonstrate looking at our containers

Questions?

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment