revolves around HTTP
- still dominating the field when it comes to web APIs
- can be very well optimized for a use case
- easily cachable, discoveravle, and simple to use by clients
api that tries to answer too many use cases
- hard to manage due to how coupled they are to different clients (browser, mobile, IOT, etc)
Solution? Add more endpoints
let’s say we want to build an API that will allow us to fetch
a products
resource
browser
GET /products
gaming console
GET api/playstation/products
mobile
GET api/mobile
the problem?
another approach: one endpoint per use case
browser
GET api/products?version=browser
gaming console
GET api/products?version=gaming
mobile
GET api/products?version=mobile
or make it more generic via partials
GET api/products?partial=full
GET api/products?partial=minimal
allow clients to select what they want from the server
the JSON API spec calls these sparse fieldsets:
GET api/products?include=author&fields[products]=name,price
here is a query language in a query parameter (inspired by google drive’s API)
GET api/products?fields=name,photos(title, metadata/height)
Most of the above have tradeoffs that include optimization and customization
netflix has found substantial limitations in the traditional one size fits all REST API approach
while effective, the problem with the OSFA approach is that its emphasis is to make it convent for the API provider not for the API consumer
this solution involved a new conpcetual layer between the typical client and server layers where client-specific code if hosted on the server
in their approach
this allows the API to give control to client developers - letting them build their client adapters on the server
facebook’s data problem:
graphql is not:
graphql is:
graphql is a specification for an API query language and a server engine capable of executing such queries
this is a query
that asks for the current user and their name
query {
me {
name
}
}
me
and name
are referred to as fields
a client sends requests like these to a graphql server (usually as a simple string)
the response looks something like this
{
"data": {
"me": {
"name": "martin"
}
}
}
notice that the response and the query are very similar shapes
if a graphql query is successful, the response always has a data
key, under this key is the reponse that the client is expecting
graphql allows clients to define requirements down to single fields, this allows them to fetch exactly what they need
another example
query {
me {
name
friends(first: 2) {
name
age
}
}
}
above we are fetching more than just a name
, we are also fetching the name
and the age
of the first 2
of my friends
- this is how we are able to traverse complex relationships. you will also notice that fields can take arguments. think of fields like functions, they can take arguments and return a certain type
the response looks something like this
{
"data": {
"me": {
"name": "martin",
"friends": [{
"name": "nikita",
"age": 3
}, {
"name": "willow",
"age": 2
}]
}
}
}
the query
keyword is not a normal field. it tells the graphql server that we want to query off the query root of the schema
at the core of any graphql server is a powerful type system that helps to express API capabilities. the type system of a graphql engine is often referred to as the schema. a common way of representing a schema is through the graphql schema definition language (SDL)
the schema definition language is the canonical representation of a graphql schema and is well defined in the spec. no matter what manguage you are running a graohql api with, the SDL describes the final schema
example:
type Shop {
name: String!
# where the shop is location, null if online only
location: Location
products: [Product!]!
}
type Location {
address: String
}
type Product {
name: String!
price: Price!
}
the most basic and crucial primitive of a graphql schema is the object type. object types desribe one concept in your graphql API. what makes them whole is when they define fields. previously (above), we defined a Shop
type that defined three fields: name
, location
, and products
, the fieldName: Type
syntax allows us to give a return type to our fields.
example: the name
field on a Shop
type returns a String. it is helpful to compare graphql fields to simple functions. fields are executed by a graphql server and return a value that maps correctly to its return type
the String
type is not user-defined, it is part of graphql’s pre-defined scalar types. graphql’s real power life in the fact fields, which can return object types of their own
example
location: Location
the location
field on the Shop
returns a type Location
which is a type that the schema defines. to see what fields are available on a Location
type we can look at the Location
type definition
type Location {
address: String!
}
the Location
type defines one address
field which returns a String
now we will see that a field can return an object type of its own
a graphql server can execute queries like these because at each level in the query - it is able to validate the client requirements against the defined schema
query {
# !. the shop field returns a `Shop` type
shop(id: 1) {
# 2. field location on the `Shop` type
# Returns a `Location` type
location {
# 3. field address exists on the `Location` type
# Returns a String
address
}
}
}
if you will notice above, we know that our Shop
type has a location
field and that our Location
type has an address
field; however, where is the shop
field coming from?
a graphql schema must be defined using type and fields to describe its capabilities
a graphql schema must always define a Query Root
(a type that defines the entry point to possibilities)
we usually call this type a Query
type Query {
shop(id: ID): Shop!
}
the Query
type is implicitly queried whenever you make a graphql api request
{
shop(id: 1) {
name
}
}
this is valid because it implicitly asks for the shop
field on the Query Root
even though we did not query for that particular field that returned a Query
type first
a Query Root
has to be defined on a graphql schema, and there are two other types of roots that can be defined, a Mutation
and a Subscription
root
type Query {
shop(id: ID!): Shop!
}
a graphql field can define arguments just like a function
the graphql server uses these arguments at the runtime resolution of the field
these fields are defined between parentheses after the field name and you can have as many of them as you like
type Query {
shop(owner: String!, name: String!, location: Location): Shop!
}
arguments, like fields, can define a type which can either be a scalar
type or an input
type
input types are similar to types, but they are declared in a different way, using the input
keyword
type Product {
price(format: PriceFormat): Int!
}
input PriceFormat {
displayCents: Boolean!
currency: String!
}
graphql queries can also define variables that can be used within a query
this allows clients to send variables along with a query and have the graphql server execute it instead of including it directly in the query string itself
query FetchProduct($id: ID!, $format: PriceFormat!) {
product(id: $id) {
price(format: $format) {
name
}
}
}
we gave this query an operation name (FetchProduct
)
a client would send this query with variables like this
{
"id": "abc",
"format": {
"displayCents": true,
"currency": "USD"
}
}
the server dictates the canonical name of fields but if the client wants to receive fields under another name, they can use aliases
query {
abcProduct: product(id: "abc") {
name
price
}
}
above, the client requests the product
field but defines an abcProduct
alias
when the client executes the query, it will get back the field as if it was named abcProduct
{
"data": {
"abcProduct": {
"name": "shirt",
"price": 25
}
}
}
this is useful when requesting the same field multiples times with different arguments
allows writing and modifying data
the entry point to the mutations of a schema is under the Mutation
root
to access the mutation
root in a graphql query, use the mutation
keyword at the top level of a query
mutation {
addProduct(name: String!, price: Price!) {
product {
id
}
}
}
we define the addProduct
mutation in a similiar way that we define fields on the query root
type Mutation {
addProduct(name: String!, price: Price!): AddProductPayload
}
type AddProductPayload {
product: Product!
}
two things make mutation
fields different from query
fields
similarly they
allow a schema to clearly define a set of values that may be returned, for fields, or passed (arguments)
they come in handy when defining an API that is easy to use by clients
type Shop {
# the type of products the shop specializes in
type: ShopType!
}
enum ShopType {
APPAREL
FOOD
ELECTRONICS
}
allow clients to expect the return type of a field to act a certain way, without returning an actual type
there are two ways to return an abstract type for fields, interfaces, and unions
Interfaces
allow us to define a contract that a concrete type implementing it must answer to
interface Discountable {
priceWithDiscounts: Price!
priceWithoutDiscounts: Price!
}
type Product implements Discountable {
name: String!
priceWithDiscounts: Price!
priceWithoutDiscounts: Price!
}
type GiftCard implements Discountable {
code: String!
priceWithDiscounts: Price!
priceWithoutDiscounts: Price!
}
we have a Product
type that implements a Discountable
interface
this means that the Product
type must define the two Discountable
fields because by implementing the interface, it must respect the contract
this allows other fields to return Discountable
directly - letting clients know they may request the fields part of that contract directly on the result (without knowing which concrete
type will be returned at runtime)
we could have a discountedItems
field that returns a list of either a Product
or GiftCard
type by directly returning an interface type of Discountable
type Cart {
discountedItems: [Discountable!]!
}
all types are expected to answer to the Discountable
contract; therefore, clients can directly ask for both of the price fields
query {
cart {
discountedItems {
priceWithDiscounts
priceWithoutDiscounts
}
}
}
if a client wants to query the other fields they must specify which concrete
type they want to select against
you can use fragment spreads or typed fragments for this
query {
cart {
discountedItems {
priceWithDiscounts
priceWithoutDiscounts
...on Product {
name
}
... on GiftCard {
code
}
}
}
}
union types are slightly different
instead of defining a certain contract, union types are more of a bag of disparate objects that a field could return
you define them by using the union
keyword
union CartItem = Product | GiftCard
type Cart {
items: [CartItem]
}
it defines no contract, only the possible concrete type that could be returned by that field; therefore, clients have to specify the expected concrete type in all cases
query {
cart {
discountedItems {
... on Product {
name
}
... on GiftCard {
code
}
}
}
}
abstract types can be useful in graphql schemas; however, they can be easily abused
allow clients to define part of a query to be refused elsewhere
to create an inline fragment to select concrete types this syntax is used ... on Product
query {
products(first: 100) {
...ProductFragment
}
}
fragment ProductFragment on Product {
name
price
variants
}
a fragment is defined by using the fragment
keyword, it takes a name and a location where it can be applied, Product
being the case shown above
an annotation that can be used on various graphql primitives
the graphql specification defines two builtin directives that are very useful: @skip
and @include
query MyQuery($shouldInclude: Boolean) {
myField @include(if: $shouldInclude)
}
the @include
directive ensures that the myField
field is only queried when the variable shouldInclude
is true
directives provide clients with a way to annotate fields in a way that can modify the execution behavior of a graphql server
directives can also accept arguments, like fields
you can also create custom directives (create a name and determine where the directive can be applied)
"""
Marks an element of a graphql schema as
only available with a feature flag activated
"""
directive @myDirective(
"""
the identifier of the feature flag that toggles this field
"""
flag: String
) on FIELD
the directive can be used by clients like this
query {
user(id: "1") @myDirective {
name
}
}
besides, being applied to queries, directives can also be used with the type system directly
this makes them useful to annotate schemas with metadata
"""
marks an element of a graphql schema as
only available with a feature flag activated
"""
directive @featureFlagged(
"""
the identifier of the feature flad that toggles this field
"""
flag: String
) on OBJECT | FIELD_DEFINITION
then we can apply this directive to schema members directly
type SpecialType @featureFlagged(flag: "secret-flag") {
secret: String!
}
with graphql, clients can ask a graphql schema for what is possible to query
graphql schemas also include introspection meta fields which allows clients to fetch almost everything about its type system
query {
__schema {
types {
name
}
}
}
{
"data": {
"__schema": {
"types": [
{
"name": "Query"
},
{
"name": "Product"
},
]
}
}
}
this allows clients to discover use cases and it also enables amazing tooling
graphiql - an interactive graphql playground
introspection is used to generate code and to validate queries ahead of time
it is also used by IDEs to validate queries while developing an app
the graphql ecosystem is growing rapidly