GraphQL

Sang Young Ha·2022년 8월 10일
post-thumbnail

what is GraphQL

  • graphQL is a query language for APIs
  • graphQL enables declarative data fetching where a client can specify exactly what data it needs from an API (you get what you ask for via queries, not the entire data set)

how it GraphQL different from REST

  • REST often times require additional requests to different endpoints and overfetch unnecessary data.
  • with GraphQL, the client can specify exactly the data it needs in a query.

The Schema Definiton Language(SDL) : the syntax for writing schemas

ex) of how to define a simple type called Person

type Person {
	name: String!
    age: Int!
    }
  • name and age are the "fields" of the given type. "!" means that the given field is required.
  • it is also possible to express relationships between types.
type Post {
	title: String!
    author: Person!
    }
  • conversely, the other end of the relationship needs to be placed on the Person type
type Person {
	name: String!
    age: Int!
    posts: [Post!]!
    }

Basic Queries

ex)

{
	allPersons {
    name
    }
}
  • the allPersons field in this query is called the "root field" of the query. Everything that follows the root field, is called the "payload" of the query. The only field that's specified in this query's payload is name.

  • This query returns a list of all persons stored in the database.

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

-> notice it only fetches the names of all persons, because allPersons only has one field called name. If the client also needs the person's age, we just need to adjust the query and add the age field.

{
	allPersons {
    name
    age
    }
}
  • one of the major advantages of GraphQL is that it allows for naturally querying nested information.
    ex) if you want to load all the posts that a Person has written,
{
  allPersons {
    name
    age
    posts {
      title
    }
  }
}

Queries with Arguments

  • in GraphQL, each field can have zero or more arguments if that's specified in the schema.
{
  allPersons(last: 2) {
    name
  }
}

Queries with Aliases

  • the result object fields match the name of the field in the query
  • you can rename the field name of the result object via "aliases"
    query:
{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}

result:

{
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

Queries with Fragments

  • fragments let you construct sets of fields, and then include them in queries where you need to.
{
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

Variables

  • since the arguments to fields can be dynamic, we can use variables to apply dynamic changes to arguments.
    1) replace the static value in the query with $variableName
    2) declare $variableName as one of the variables accepted by the query
    3) pass variableName: value in the separate, transport-specific(usually JSON) variables dictionary
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

variables
{
	"episode": "JEDI"
}

using variables inside fragments

  • you can access variables declared in the query or mutation within your fragments.
query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
  • Int = 3 gives the default value for our variable $first.

Inline Fragments

  • GraphQL schemas include the ability to define interfaces and union types.
  • inline fragments are used to access data on the underlying concrete type when querying a field that returns an interface or a union type.
    ex)
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
//variable
{
	"ep": "JEDI"
}
  • in the above example, the hero field returns the type Character, which can be either a Human or a Droid depending on the episode arguemnt.
  • in the direct selection, you can only ask for fields that exist on the Character inteface, such as name.
  • an inline fragment with a type condition can be used to ask for a field on the concrete type.
  • with the first fragment "... on Driod" the primaryFunction field will be executed if hero field returns Droid type.

Meta fields

  • there could be some situations where you do not know what type you will get back, in which you need to determine how to handle that data on the client.
  • with a meta field "__typename" you can get the name of the object type.
    ex)
{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
    ... on Droid {
      name
    }
    ... on Starship {
      name
    }
  }
}

//result
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo"
      },
      {
        "__typename": "Human",
        "name": "Leia Organa"
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1"
      }
    ]
  }
}

Operation name

  • you can give names to your queries and mutations to make your code less ambiguous by giving the keyword query as operation type and "yourOperationName" as operation name.
query HeroNameAndFriends {
	hero {
    	name
        friends {
        	name
       }
     }
 }
  • the operation type is either query, mutation, or subscription.

Directives

  • directives can be used to apply dynamic changes to the structure and shape of our queries using variables.
    ex)
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

//variables
{
  "episode": "JEDI",
  "withFriends": false
}
  • a directive can be attached to a field or fragment inclusion, and can affect execution of the query in any way the server desires.
  • the core GraphQL specification includes two directives.
    1) @include(if: Boolean) -> only include this field in the result if the argument is true.
    2) @skip(if: Boolean) -> skip this field if the argument is true.

Mutation

  • queries can be implemented to cause a data write, but similar to the convention of REST which states that you should not modify data with GET requests, you should not modify data with queries in GraphQL. Instead, use mutation.
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

//variables
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}
  • from the above example, createReview field returns the stars and commentary fields of the newly created review.
  • note that the review variable is NOT a scalar. It is an "input object type", which is a special kind of object type that can be passed in as an argument.

Multiple fields in mutations

  • just like queries, mutations can hold multiple fields.
    IMPORTANT distinction between queries and mutations
    --> While query fields are executed in parallel, mutation fields run in series, one after the other
    -> meaning if we send two mutations in one request, the first one is guaranteed to finish before the second mutation begins.

Writing Data with Mutations

  • you can change the data stored in the database from the client side. it can be done with mutations.
    1) you can create new data, 2) update exisiting data, 3) delete existing data

ex) how to create a new Person

mutation {
  createPerson(name: "Bob", age: 36) {
    name
    age
  }
}

-> notice the syntax is similar to that of queries, but mutations require the keyword "mutation".
-> createPerson is the root field of the above mutation.

  • being able to also query information when sending mutations can be a very powerful tool that allows you to retrieve new information from the server in a single round trip.

-> server response for the above mutation

"createPerson": {
  "name": "Bob",
  "age": 36,
}
  • GraphQL types have unique ID's that are generated by the server when newt objects are created
type Person {
  id: ID!
  name: String!
  age: Int!

Realtime Updates with Subscription

  • realtime connection to the server allows you to get immediately informed about important events.
  • when a client subscribes to an event, it will initiate and hold a steady connection to the server.
  • whenever that subscribed event happens, the server pushes the corresponding data to the client.
  • it represents a stream of data instead of a typical "request-response-cycle".
    ex) of a subscription
subscription {
  newPerson {
    name
    age
  }
}
  • with this subscription sent from a client, a connection is open between a server and a client.
  • whenever a new mutation is performed that creates a new Person, the server sends the information about this person over to the client.
{
  "newPerson": {
    "name": "Jane",
    "age": 23
  }
}

Schemas and Types

Type language

  • with GraphQL schema language, it can be written in any language.

Object types and fields

  • the most basic components of a GraphQL schema are object types, which just represent a kind of object you can fetch from your service, and what fields it has.
    ex)
type Character {
	name: String!
    appearsIn: [Episode!]!
}
  • "Character" is a GraphQL Object Type, meaning it is a type with some fields. Most of the types in GraphQL schema will be oject types.
  • "name" and "appearsIn" are fields on the "Character" type. In any part of of a GraphQL query that operates on the "Character" type has access to "name" and "appearsIn" fields.
  • "String" is one of the built-in scalar types. ! means not this field is non-nullable.
  • "[Episode!]!" represents an array of "Episode" objects which is also non-nullable(meaning an array of 0 or more items are always expected when the "appearIn" field is queried).
  • since Eisode! is also non-nullable, you can always expect every item of the array to be an Episode object.

Arguments

  • every field on a GraphQL oject type can have 0 or more arguments.
  • unlike Javascript where functions take a list of ordered arguments, all arguments in GraphQL are passed by name.
    ex)
type Starship {
	id: ID!
    name: String!
    length(unit:  LenghUnit = METER): Float
}
// the field "length" has one defined argument "unit"
  • arguments can be required or optional, when it is optional, we can define a default value in this case is "METER".

The Query and Mutation types

  • there are two types are are special within a schema,
schema {
	query: Query
    mutation: Mutation
 }
  • query type is required for all GraphQL services while mutation type could be optional.
  • they are just like a regular object type, but they define the entry point of every GraphQL query which makes them special.
query {
	hero {
    	name
    }
    droid(id: "2000") {
    name
    }
}

-- for a query like the above, it means that the GraphQL service needs to have a "Query" type with "hero" and "droid" fields.

type Query {
	hero(episode: Episode): Character
    droid(id: ID!): Droid
}

Scalar types

  • a GraphQL object type has a name and fields, and at some point those fields have
    to resolve to some concrete data.
  • in order for this to happen, scalar types come into play: they represent the leaves of the query.
{
  hero {
    name
    appearsIn
  }
}
  • from the above query, the "name" and "appearsIn" fields will resolve to scalar types, and they are the leaves of the query (they do not have any sub-fields)

  • A set of default scalar types from GraphQL
    1) Int: A signed 32-bit integer.
    2) Float: A signed double-precision floating-point value.
    3) String: A UTF-8 character sequence.
    4) Boolean: true or false
    5) ID: represents a unique identifier, often used to refetch an object or as the key for a cache. It behaves like String, but defining it as an ID instead of a String shows that it is not intended to be human-readable.

  • you can make a custom scalar type which we need to defind how that type should be serialized, deseralized and validated.

Enumeration(a.k.a Enums) types

  • special kind of scalar that is restricted to a particular set of allowed values.
  • with the enum type, you can:
    1) validate that any arguments of this type are one of the allowed values.
    2) communicate through the type system that a field will always be one of a finite set of values.

ex)

enum Episode {
	NEWHOPE
    EMPIRE
    JEDI
}
  • this menas that whereever we use the type Episode in our schema, we expect it to be EXACTLY one of "NEWHOPE", "EMPIRE", or "JEDI".

Non-Null(!)

  • object types, scalars and enums are the only kinds of types you can define in GraphQL, but you can apply additional type modifiers that affect VALIDATION of those values.
    ex)
type Character {
	name: String!
    appearsIn: [Episode]!
}
  • with the "!" type modifier, the server always expects to return a non-null value for the name field, and if it does receive a null value it will trigger a GraphQL execution error.
  • the Non-Null type modifier "!" can also be used when defining areguments for a field, which will cause the GraphQL server to return a validation error if a null value is passed as that argument, whether in the GraphQL string or in the variables.
    ex)
query DroidById($id: ID!) {
  droid(id: $id) {
    name
  }
}
// variable
{
	"id": null
}

// will throw an error as a result
{
  "errors": [
    {
      "message": "Variable \"$id\" of non-null type \"ID!\" must not be null.",
      "locations": [
        {
          "line": 1,
          "column": 17
        }
      ]
    }
  ]
}

List

  • list can be marked as a List with "[ ]" type modifier.
  • validation step will expect an array for this value.
  • "!" and "[ ]" can be comebined like below to indicate that it is a list of non-null strings.
myField: [String!] 

-> this means that the list itself can be null, but it cannot have any null members.

myField: null // valid
myField: [] // valid
myField: ['a', 'b'] // valid
myField: ['a', null, 'b'] // error
  • another example below indicates that the list itself cannot be null, but it can contain null values.
myFeild: [String]!

myField: nulll // error
myField: [] // valid
myField: ['a','b'] // valid
myField: ['a', null, 'b'] // valid

Interfaces

  • an Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface.

  • ex) you could have an interface "Character" that represents any character in the Star Wars trilogy:

interface Character {
	id: ID!
    name: String!
    friends: [Character]
    appearsIn: [Episode]!
}
  • this means that any type that implements "Character" interface needs to have these Exact fields, with the same arguments and return types.

ex) of some types implementing "Character" interface

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  starships: [Starship]
  totalCredits: Int
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

--> both the Human and Droid types have all the fields from the Character interface, but also has extra fields, such as "totalCredits", "starships", and "primaryFunction" that are specific to that particular type of character.

When to use interfaces?

  • they are useuful when you want to return an object or set of objects, but those might be of several different types.

ex)

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    primaryFunction
  }
}
//variable
{
	"ep": "JEDI"
}
  • above query produces an error because the Character interface does not include primaryFunction, which is field that belongs to the type Droid only.

  • an inline fragment can be used to ask for a field on a specific object type.
    ex)

// query
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}

// variable
{
	"ep": "JEDI"
}

// returns
{
  "data": {
    "hero": {
      "name": "R2-D2",
      "primaryFunction": "Astromech"
    }
  }
}

Union types

  • union types are very similar to intefaces, but they do not specify
    any common fields between the types.
    ex)
union SearchResult = Human | Droid | Starship
  • whereever we return a "SearchResult" type in our schema, we could access one of "Human", a "Droid", or a "Starship".

  • members of a union type need to be concrete object types; meaning members cannot be of interfaces or other unions.

  • with union types, inline fragments are REQUIRED to be able to query any fields at all.
    ex)

// query
{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

//result
{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "__typename": "Human",
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}
  • the "__typename" field resolves to a string which lets you differentiate different data types from each other on the client.
  • in this case, since Human and Droid share a common "Character" interface, you can query their common fields in one place rather than having to repeat the same fields across multiple types:
{
  search(text: "an") {
    __typename
    ... on Character {
      name
    }
    ... on Human {
      height
    }
    ... on Droid {
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

note that Starship still has a name field because Starship is NOT a Character

Input types

  • a type of complex objects that can be passed in as an argument into a field.
    input types are particularly valuable in the case of mutations

Defining a Schema

  • a schema specifies the capabilities of the API and defines how clients can request the data. It is like a contract between back/front end.
  • it is simply a collection of GraphQL types.
  • there are some special root types.
type Query { ... }
type Mutation { ... }
type Subscription { ... }
  • the above three types are the entry points for the requests sent by the client.
  • in order to enable the example "allPersons" query that we saw above, we need to write like below.
type Query {
	allPersons: [Person!]!
}
  • with type Query being a root type and the allPersons being a root field.

  • to define arguments of the allPersons field.

type Query {
  allPersons(last: Int): [Person!]!
}
  • for the createPerson mutation,
type Mutation {
  createPerson(name: String!, age: Int!): Person!
}
  • for the newPerson subscription,
type Subscription {
  newPerson: Person!
}
  • to sum things up, the entire schema for the above example,
type Query {
  allPersons(last: Int): [Person!]!
  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!
}

0개의 댓글