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
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
- traverse lists of objects with a consistent field pagination model
- https://graphql.org/learn/pagination/
- can splice lists, return specific regions of long lists
- 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
}
}