top of page

Uge 38: GraphQL

  • Writer: Kenneth H Sørensen
    Kenneth H Sørensen
  • Sep 20
  • 6 min read

ree

The basics


Jeg vil gennemgå de grundlæggende begreber i GraphQL for at forstå, hvordan det fungerer, og reflektere over forskelle i forhold til REST.



Intro til GraphQL



GraphQL

  • Et query-sprog for API'er - klient beskriver præcis hvilke data den ønsker

  • Et runtime på serveren - udfører queries ved hjælp af en type-defineret kontrakt (schema)

  • Ikke bundet til en database  - kan bruges med SQL, NoSQL, API-kald, filer eller andet

  • Felt-for-felt eksekvering

    • klient anmoder om præcis data

    • server leverer via funktioner / metoder (resolvers)



Kernekoncepter (uafhængigt af værktøjer)

  • Schema  - definerer types og fields  - kontrakten mellem klient og server

  • Resolvers  - funktioner/metoder der leverer de enkelte felters værdier

  • Context  - et objekt delt pr. request, f.eks med auth-info, DB-forbindelser, caches

  • Execution  - flow er altid:

    1. Query

    2. Valider

    3. Kald resolvers

    4. Byg JSON (samme form som query)




Eksempel - hent eget navn

Problem / behov

  • En klient vil kende navnet på den bruger, der er logget ind.



Query

GraphQL gør det muligt at spørge præcist om de felter, der er brug for.

{
  me {
    name
  }
}
  • GraphQL forespørgsel skrevet i GraphQL-sproget

    Sproget klienten bruger for at sige “jeg vil have felt name fra me



Request

Hele HTTP-requesten, som bruges til sende query til serveren.


Klient sender en HTTP POST til serverens GraphQL-endpoint (f.eks /graphql)

  • F.eks fra browser, mobil (ofte via biblioteker som Apollo Client) eller fra test-værktøjer som Postman under udvikling

POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer <jwt-token>
{
  "query": "{ me { name } }"
}
  • Header

    • POST /graphql: endpointet for GraphQL

    • Content-Type: application/json: body (content) er JSON

    • Authorization: Bearer ...: JWT-token til auth

  • Body

    • Indeholder feltet "query", hvor GraphQL-sproget { me { name } } er skrevet som en streng



Schema - typer og felter

Et schema definerer, hvilke felter en klient kan spørge efter.


type Query {
  me: User
}

type User {
  name: String
}
  • Query (root type for READ) har field me, som returnere User

  • User har field name, som returnerer String


Schema er en kontrakt - beskriver form og tilgængelige felter (IKKE hvordan data hentes).



Resolvers - logikken bag felterne


En resolver er en funktion / metode, der leverer værdien til et felt under eksekvering.

  • Hvert felt i schema kan have en resolver

  • Default-resolver i GraphQL kan automatisk læse værdier direkte fra objektets properties - eksplicit resolver er ikke altid nødvendig

  • Når der er brug for logik (f.eks databaseopslag, autorisation eller beregnede værdier) - eksplicit resolver defineres


Fra dokumentationen (JavaScript):

// Resolver for the me field on the Query type
function resolveQueryMe(_parent, _args, context, _info) {
  return context.request.auth.user;
}

// Resolver for the name field on the User type
function resolveUserName(user, _args, context, _info) {
  return context.db.getUserFullName(user.id);
}

I C# (Hot Chocolate) skrives det som klasser og metoder:

public class Query
{
    public User Me([Service] IHttpContextAccessor httpContext)
    {
        return new User 
        { 
		   // Id for authenticated user from claims
            Id = httpContext.HttpContext.User.FindFirst("id").Value
        };
    }
}

public class User
{
    public string Id { get; set; }

    public string GetName([Parent] User user, [Service] MyDb db)
    {
        return db.GetUserFullName(user.Id);
    }
}
  • Query.Me()  - resolver for root-feltet me

  • User.GetName()  - resolver for feltet name på typen User

  • [Parent] - giver adgang til objektet fra det forrige felt (User returneret fra me)

  • [Service] - injicerer afhængigheder (f.eks. IHttpContextAccessor, databaseforbindelse)




Execution

Eksekveringsflow for query { me { name } }

  • Query modtages

    • klientens forespørgsel kommer ind som en streng i HTTP-body

  • Valideres mod schema

    • serveren tjekker, om de efterspurgte felter findes i schemaet

    • ellers returner fejl

  • Kald resolvers

    • Felt-for-felt

    • Query.Me() kaldes og returnerer User { Id = "42" }

    • User.GetName() kaldes med User som [Parent] og slår navnet op i databasen

  • Byg svar

    • resultatet samles i samme struktur som query


Response

{
  "data": {
    "me": {
      "name": "Luke Skywalker"
    }
  }
}


Delkonklusion

Eksemplet ovenfor viser kun en lille del af GraphQL (en Query med felterne me og name).

I praksis kan et schema indeholde flere typer og tre forskellige operationer.




Schema og Types

Et schema er som sagt kontrakten mellem klient og server. Det beskriver, hvilke typer data der findes, og hvilke felter klienten kan spørge på.


GraphQL’s typesystem er kernen - det definerer alt, hvad der kan forespørges i API'et.


Object types

  • Den mest almindelige type i GraphQL

  • Definerer et objekt og de felter, det har

type Character {
  name: String!
  appearsIn: [Episode!]!
}

Her har Character to felter: name (en streng, altid ikke-null) og appearsIn (en liste af episoder).



Scalars

  • Felter ender altid i en scalar type - de er bladene i træet

  • Standard scalars: Int, Float, String, Boolean, ID

  • Man kan definere custom scalars, f.eks Date



Enums

  • Som scalars, men begrænset til et fast sæt værdier.

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Type modifiers

  • ! Non-Null - feltet må ikke være tomt

  • [] List - feltet returnerer en liste

  • Kan kombineres, f.eks [String!]! (liste, ikke null, af strings som heller ikke er null)



Interfaces

  • Definerer et fælles sæt felter, som flere typer skal implementere

    Gør det muligt at spørge på tværs af typer

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

Typer som kunne implementere 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
}

Brug inline fragment for at differentiere mellem objekter

{
  hero(episode: JEDI) {
    name
    ... on Droid {
      primaryFunction
    }
  }
}


Unions

  • En type, der kan være én af flere forskellige objekttyper


Eks - Schema for Searchresult - Alle steder hvor Searchresults returneres kan man få de 3 typer

union SearchResult = Human | Droid | Starship

Operation

  • Klienten spørger efter search(text: "an")

  • Resolver for search bestemmer hvordan argumentet bruges (f.eks på om navn har "an)

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

Response

{
  "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}
    ]
  }
}
  • __typename - special meta felt der returnere navnet på typen




Input Object types

  • Bruges til at sende komplekse input, især i mutationer (f.eks oprette / opdatere et objekt)


input i stedet for type

input ReviewInput {
  stars: Int!
  commentary: String
}

type Mutation {
  createReview(episode: Episode, review: ReviewInput!): Review
}

Giver mulighed for at sende flere felter samlet i ét argument i stedet for mange små.


Operation

mutation {
  createReview(
    episode: JEDI, 
    review: {
      stars: 5
      commentary: "This is a great movie!"
    }
  ) {
    stars
    commentary
  }
}

Response

{
  "data": {
    "createReview": {
      "stars": 5,
      "commentary": "This is a great movie!"
    }
  }
}
  • Komma efter 5 i respons fordi det er JSON

  • Men i GraphQL bruger object literals IKKE komme mellem felter



Direktiv(er)

Ligesom annotations eller metadata i andre sprog - små instruktioner, der ændrer hvordan et felt, en type eller en operation behandles.


  • Starter altid med @ efterfulgt af navnet på direktivet

  • Kan have argumenter ligesom felter

  • Kan bruges på schema-niveau (type system directives) eller på query-niveau (executable directives)


Indbyggede direktiver


@deprecated

  • Markerer at et felt eller enum-værdi ikke længere bør bruges

  • Hjælper klienter med at migrere til nye felter

type User {
  fullName: String
  name: String @deprecated(reason: "Use `fullName`.")
}

@include / @skip

  • Bestemmer om et felt skal returneres afhængigt af en betingelse

query getUser($withPicture: Boolean!) {
  me {
    name
    picture @include(if: $withPicture)
  }
}
  • Hvis variablen withPicture = false, udelades picture fra svaret




Refleksion

Når jeg sammenligner GraphQL med REST, kan jeg se nogle klare fordele.

REST har fungeret godt til mange systemer, men vi har i tidligere projekter oplevet problemer med over-fetching og payloads, der er større end nødvendigt, fra eksterne API producers.


REST erfaringer

  • API'et returnerer ofte en fast struktur

  • Vi får mange felter, som vi ikke har brug for

  • Klienten skal selv sortere og filtrere i data


GraphQL fordele

  • Klienten kan præcist spørge efter de felter, den har brug for (meget fleksibel)

    I en og samme request kan klienten

    • hente forskellige typer data

    • nøjes med enkelte felter fra et objekt

    • kombinere dybe relationer og flade data uden ekstra requests

    • få data tilbage i samme form som query

  • Undgår både over-fetching og under-fetching

  • Payloads bliver mindre og mere effektive

  • API'et kan udvikle sig uden versionering - felter kan få @deprecated direktiv i stedet

  • GraphQL er mere komplekst at sætte op end et simpelt REST-endpoint, men fleksibiliteten og kontrol på klientsiden kan gøre det til en stærk løsning i større systemer


Hvornår REST stadig er godt

  • Små API'er med få felter og simple endpoints

  • Når caching via HTTP er vigtig

  • Når klienten ikke har brug for fleksible queries

  • Den faste struktur kan være hurtigere, hvis behovet er lille

  • Mere kendt og ofte lettere at sætte op, især til små projekter



Konklusion

Valget mellem REST og GraphQL afhænger af behovet. REST kan være simpelt og effektivt i små systemer, mens GraphQL giver fleksibilitet og kontrol i større løsninger med komplekse databehov. Det er derfor vigtigt at kunne vurdere konteksten, inden man vælger teknologi.



Videre plan

  • Læse videre i GraphQL Learn (Queries, Mutations, Subscriptions ...)

  • Bygge et simpelt GraphQL endpoint i C#

  • Kigge på Kafka og forskellen til GraphQL (asynkron events vs. synkrone queries)

  • Undersøge storage og pre-signed URLs til Ingestion Service



Ressourcer


GraphQL Learn

Dokumentation


GraphQL

Specifikation


Evt. videre læsning

GraphQL

Training Courses



bottom of page