Query language for API that enables declarative data fetching

general

  • client specifies exactly what data it needs from an API

  • exposes a single endpoint and returns exactly what is asked for

    • send a single query with a “type” , and get back a JSON where requested requirements are fulfilled
  • Benefits:

    • optimizes data access - fine-grained data fetching
    • enables API reuse
    • reduces network calls
    • reduce payload size
    • optimize client-side data-fetching and caching
    • decouple frontend from backend
    • strongly-type domain as a single, interconnected interface
    • resolver functions help measure performance and system bottlenecks
  • Pros compared to REST

    • one endpoint instead of multiple
    • allows multiple different frontend clients to access exactly what they need, instead of trying to maintain one API that fits reqs of all
    • faster development - doesn’t need to change how data is exposed - more flexible
  • types are defined in a schema using SDL (schema definition language)

    • contract between client and server
  • common in microservice gRPC architecture

  • Apollo Server

SDL

  • ! specifies required fields
  • types can have relationships
type Person {
	name: String!
	age: Int!
	posts: [Post!]!
}

type Post {
	title: String!
  author: Person!
}
  • queries have a root field, and payload
// query
{
	{
		allPersons {
			name
	}
}

// response
{
	"allPersons": [
		{"name": "Johnny"},
		{"name": "Sarah"}.
		{"name": "Alice"
	]
}

  • can naturally query nested info
  • mutations are for changing data stored in backend
mutation {
	createPerson(name: "Bob", age: 36) {
		name
		age
  }
}
  • can query for info when sending mutations - single trip

  • graphQL types have unique IDs generated by server when new objects are created

  • subscriptions: when client subscribes to an event, it initiates and holds a steady connection to the server

subscription {
	newPerson {
		name
		age
	}
}
  • when a new mutation is performed that creates a new person, server will send info about it back to client

defining schema

  • specify capabilities of API and how clients can request data
  • generally, a collection of types
  • special root types = entry points for client requests
type Query {
	allPersons(last: Int): [Person!]!
}

type Mutation {
	createPerson(name: String!, age: Int!): Person!
}

type Subscription {
	newPerson: Person!
}
  • full schema example:
type Query {
  allPersons(last: Int): [Person!]!  // can take optional int arg
  allPosts(last: Int): [Post!]!
}

type Mutation {
  createPerson(name: String!, age: Int!): Person!
  updatePerson(id: ID!, name: String!, age: String!): Person!
  deletePerson(id: ID!): Person!
}

type Subscription {
  newPerson: Person!
}

type Person {
  id: ID!
  name: String!
  age: Int!
  posts: [Post!]!
}

type Post {
  title: String!
  author: Person!
}

architecture

  • in a GQL server with a connected database
    • single web server implementing GQL
    • resolve query = when query arrives at server, payload is read, and info is fetched from DB
    • response object is contructed and returned
    • transport-layer-agnostic - could be used with any network protocol, doesn’t care about database format
  • GQL server acts as thin layer in front of third party systems, integrating through a single API
    • unifies existing systems and hides their complexity
    • new client just talks to the server, does not care about DB type or data sources
  • very flexible, pushes all data management complexity to the server

resolvers

  • query payload has a set of fields
    • in server, each field corresponds to one resolver function
// query

query {
	User(id: "abc") {
		name
		followers(first: 5) {
			name
			age
		}
	}
}

// resolvers

User(id: String!): User

name(user: User!): String

age(user: User!): Int

friends(first: Int, user: User!): [User!]!

  • cache data fetched from server - flatten and store individual records
  • query is traversed field-by-field, executing each resolver

best practices

  • gql typically served over http with a single endpoint
  • responds with JSON usually
    • compresses well
    • accept-encoding
  • avoid versioning - only returns explicitly requested data, so new capabilities can be added w/o creating breaking change - VERSIONLESS API
  • every field is nullable by default
    • be MINDFUL of making fields nullable
    • scenarios like when db goes down, async action fails, eception thrown…
    • if every field is nullable, a field could return null instead of complete request failure
    • GQL provides non-null variants of types, so a field will never return null if it is requested - if an error occurs, the prev parent field will be null instead…
  • pagination
  • batching: multiple requests from backend are collected over a short amount of time and dispatched in a single request to underlying DB
    • prevent repeatedly loading data from DB
  • model business domain as graph
    • continuous graph of entities bound by relationships
    • before adding new root queries, consider if it can be solved via a traversal
      • driven by needs of frontend and client
    • should be intuitive and difficult to misuse by the client!

more concepts

  • Fragment: collection of fields on a specific type
    • improves structure and reusability of gql code
type User {
  name: String!
  age: Int!
  email: String!
  street: String!
  zipcode: String!
  city: String!
}

// enable querying by addressDetails instead of all the fields

fragment addressDetails on User {
  name
  street
  zipcode
  city
}
  • can parameterize fields with args

type Query {
  allUsers(olderThan: Int = -1): [User!]!
}

{
  allUsers(olderThan: 30) {
    name
    age
  }
}