Part 1: Introduction to GraphQL and SDL
How I Came to GraphQL
For years I built REST APIs and assumed the problem was just clients being needy. The mobile app wants a stripped-down user summary? Make a new endpoint. The dashboard needs data from three services? Make three sequential requests. I treated every extra endpoint as a product requirement rather than an API design problem.
The pivot happened while working on a platform that served both a React frontend and a mobile app. The REST API I had designed was correct by REST standards but neither client could use it efficiently. The web frontend was making six requests per page load; the mobile app was downloading 40 fields when it used three. I added projection parameters, created view-specific endpoints, and the API became harder to maintain with every sprint.
A colleague suggested GraphQL. I dismissed it at first โ "it's just REST with extra steps." It is not. Once I understood that GraphQL moves the query responsibility to the client and the type system makes that safe, I rebuilt the API in GraphQL and the mobile team stopped asking for new endpoints entirely.
This part covers what GraphQL is, how it differs from REST, and how to read and write Schema Definition Language (SDL). By the end you will have a running Apollo Server with TypeScript.
What is GraphQL?
GraphQL is three things:
A query language for APIs โ clients describe what data they need in a structured syntax
A type system โ the server defines all available data and operations in a schema written in SDL
A runtime โ the server validates every incoming query against the schema and executes only what was asked
The GraphQL specification was open-sourced by Facebook in 2015. It is not tied to any database, transport, or language. Any server that implements the spec can be called a GraphQL server.
Core Concepts
The Schema is the Contract
In GraphQL, the schema is the single source of truth. It defines every type, every field, and every operation available. Unlike REST where the contract lives in documentation (OpenAPI, README files, or institutional memory), the GraphQL schema is the contract and is machine-readable.
Clients Ask for Exactly What They Need
The defining behaviour of GraphQL: clients specify the exact fields they want, the server returns precisely those fields.
REST returns the full resource shape:
GraphQL returns only what was requested:
Single Endpoint
All GraphQL operations go to a single endpoint โ typically POST /graphql. The operation type (query, mutation, subscription) and the requested fields are in the request body, not the URL. This is a significant mental shift from REST.
The Schema Definition Language (SDL)
SDL is the language used to define a GraphQL schema. It is human-readable, language-agnostic, and becomes the specification for your API. Every tool in the GraphQL ecosystem reads SDL.
Scalar Types
GraphQL has five built-in scalar types:
String
UTF-8 string
Int
32-bit signed integer
Float
Double-precision floating point
Boolean
true or false
ID
Unique identifier โ serialised as a string
Custom scalars are covered in Part 2.
Non-Null and Lists
By default, every field in GraphQL is nullable. The ! suffix marks a field as non-null โ the server guarantees it will never return null for that field.
Reading list nullability:
[Role!]!โ the list itself is non-null AND every item in the list is non-null[String!]โ the list may be null, but if present, items are non-null[String]โ both the list and its items may be null
Object Types
Input Types
Input types are the only way to pass complex objects as arguments to mutations. Output types (e.g. Product) cannot be reused as inputs.
Note that UpdateProductInput uses nullable fields โ only the fields provided will be updated.
Queries, Mutations, and Subscriptions
These three operation types map to the Query, Mutation, and Subscription root types:
Arguments and Default Values
Fields can have arguments with optional default values:
GraphQL vs REST vs gRPC
From personal experience, here is when I reach for each:
Primary use
Frontend-facing APIs, flexible queries
Standard CRUD APIs, public APIs
Internal microservice communication
Request shape
Client-defined (query language)
Server-defined (fixed response)
Strict schema (proto)
Over-fetching
Eliminated by design
Common
Eliminated by design
Under-fetching
Eliminated by design
Common
Eliminated by design
Real-time
Subscriptions (WebSocket)
SSE / polling workaround
Native streaming
Browser support
Excellent (HTTP POST)
Excellent
Needs proxy (gRPC-web)
Tooling maturity
Excellent
Excellent
Good
Learning curve
Medium
Low
Medium-High
Inter-service calls
Adds overhead vs gRPC
Common, well-understood
Best performance
When I choose GraphQL over REST: frontend-facing APIs where multiple clients (web, mobile, public API) need different field subsets, or where the number of "combination" endpoints is growing uncontrollably.
When I stick with REST: simple CRUD with a single client, public webhooks, file upload/download, or when response caching at the CDN level is critical.
When I use gRPC instead: service-to-service internal communication where performance and strict contracts matter more than query flexibility.
Setting Up Apollo Server 4 with TypeScript
Let me walk through the complete setup I use as a starting point.
Project Initialisation
package.json
tsconfig.json
Your First Schema and Server
Running Your First Query
Open http://localhost:4000 in a browser โ Apollo Server serves a built-in sandbox where you can write and run queries interactively.
Or with curl:
Response:
How GraphQL Executes a Query
Understanding execution helps debug unexpected resolver behaviour.
When the server receives:
Execution proceeds field-by-field:
GraphQL validates the query against the schema (is
producta valid query field? does it accept anidargument? arenameandpricevalid fields onProduct?)The
Query.productresolver runs withargs = { id: "1" }Its return value (
{ id: "1", name: "Widget Pro", price: 29.99, inStock: true }) becomes the parent for the next levelThe
Product.nameresolver runs โ but since there is no custom resolver forname, GraphQL uses the default resolver:parent.nameSame for
Product.priceinStockis in the parent object but was not requested โ it is omitted from the response
The default resolver (parent[fieldName]) handles most scalar fields automatically. You only need to write custom resolvers when:
The field name in GraphQL differs from the field name in the data source
The field requires a database query (related entities)
The field applies business logic before returning
What's Next
You now understand:
What GraphQL is and why it exists
The SDL building blocks: scalars, types, inputs, enums, non-null, lists
Queries, mutations, and subscriptions
How GraphQL executes a query
A running Apollo Server 4 with TypeScript
In Part 2, we go deeper into the type system โ interfaces, unions, custom scalars, and how to design a schema that models a real domain and evolves without versioning.
Last updated