GraphQL Schema
Your GraphQL server uses a schema to describe the shape of your available data. This schema defines a hierarchy of types with fields that are populated from your back-end data stores. The schema also specifies exactly which queries and mutations are available for clients to execute.
This article describes the fundamental building blocks of a schema and how to create a schema using GraphQL for Delphi.
The schema definition language
The GraphQL specification defines a human-readable schema definition language (or SDL) that you use to define your schema and store it as a string.
Here's a short example schema that defines two object types: Book
and Author
:
type Book {
id: ID
name: String
author: Author
}
type Author {
id: ID
name: String
books: [Book]
}
A schema defines a collection of types and the relationships between those types. In the example schema above, a Book
can have an associated author, and an Author
can have a list of books.
Because these relationships are defined in a unified schema, client developers can see exactly what data is available and then request a specific subset of that data with a single optimized query.
Note that the schema is not responsible for defining where data comes from or how it's stored. It is entirely implementation-agnostic.
Field definitions
Most of the schema types you define have one or more fields:
# This Book type has two fields: name and author
type Book {
id: ID # record ID
name: String # returns a String
author: Author # returns an Author
}
A field can return a list containing items of a particular type. You indicate list fields with square brackets [], like so:
type Author {
id: ID
name: String
books: [Book] # A list of Books
}
By default, it's valid for any field in your schema to return null
instead of its specified type. You can require that a particular field doesn't return null
with an exclamation mark !
, like so:
type Author {
id: ID! # Can't return null
name: String! # Can't return null
books: [Book]
}
These fields are non-nullable. If your server attempts to return null for a non-nullable field, an error is thrown.
With a list field, an exclamation mark !
can appear in any combination of two locations:
type Author {
id: ID! # Can't return null
name: String! # Can't return null
books: [Book!]! # This list can't be null AND its list *items* can't be null
}
- If
!
appears inside the square brackets, the returned list can't include items that are null. - If
!
appears outside the square brackets, the list itself can't be null. - In any case, it's valid for a list field to return an empty list.
The Query
type
The Query
type is a special object type that defines all of the top-level entry points for queries that clients execute against your server.
Each field of the Query
type defines the name and return type of a different entry point. The Query
type for our example schema might resemble the following:
type Query {
author(id: ID!): Author
book(id: ID!): Book
books: [Book!]!
authors: [Author!]!
}
This Query
type defines four fields. author
and book
fields return a record of the corresponding type by its id
, books
and authors
fields return a list of the corresponding type.
With a REST-based API, this would probably be returned by different endpoints (e.g., /api/books
, /api/authors
, /api/books/:id
, /api/authors/:id
). The flexibility of GraphQL enables clients to query both resources with a single request.
Structuring a query
Based on our example schema so far, a client could execute the following query, which requests both a list of all book names and a list of all author names:
query GetBooksAndAuthors {
books {
name
}
authors {
name
}
}
Our server would then respond to the query with results that match the query's structure, like so:
{
"data": {
"books": [
{
"name": "War and Peace"
},
...
],
"authors": [
{
"name": "Leo Tolstoy"
},
...
]
}
}
The Mutation
type
The Mutation
type is similar in structure and purpose to the Query
type. Whereas the Query
type defines entry points for read operations, the Mutation
type defines entry points for write operations.
Each field of the Mutation
type defines the signature and return type of a different entry point. The Mutation
type for our example schema might resemble the following:
type Mutation {
createAuthor(name: String!): Author!
createBook(name: String!, authorId: ID!): Book!
}
This Mutation
type defines available mutations, createAuthor
and createBook
. createBook
mutation accepts a single argument (name
) and returns a newly created Author
object. createBook
mutation accepts two arguments (name
and authorId
) and returns a newly created Book
object.
Structuring a mutation
Like queries, mutations match the structure of your schema's type definitions. The following mutation creates a new Author
and requests certain fields of the created object as a return value:
mutation CreateBook {
createAuthor(name: "Leo Tolstoy") {
id
name
}
}
As with queries, our server would respond to this mutation with a result that matches the mutation's structure, like so:
{
"data": {
"createAuthor": {
"id": 1,
"name": "Leo Tolstoy",
}
}
}
Resolvers
GraphQL Server needs to know how to populate data for every field in your schema so that it can respond to requests for that data. To accomplish this, it uses resolvers.
A resolver is a function that's responsible for populating the data for a single field in your schema. It can populate that data in any way you define, such as by fetching data from a back-end database or a third-party API.
Defining Resolvers
Let's say our server defines the schema described above. We want to define resolvers for Query
, Mutation
, Author
, Book
and their fields.
GraphQL for Delphi library can map GraphQL schema definition onto Delphi class structure. Those resolvers definitions look like this:
type
TBook = class;
TAuthor = class
private
FId: Integer;
FName: string;
function GetBooks: TArray<TBook>;
public
constructor Create(AName: string);
property Books: TArray<TBook> read GetBooks;
property Id: Integer read FId;
property Name: string read FName;
end;
TBook = class
private
FId: Integer;
FName: string;
FAuthorId: Integer;
function GetAuthor: TAuthor;
public
constructor Create(AName: string; AAuthorID: Integer);
property Id: Integer read FId;
property Name: string read FName;
property Author: TAuthor read GetAuthor;
end;
TMutation = class
public
function CreateAuthor(Name: string): TAuthor;
function CreateBook(Name: string; AuthorID: Integer): TBook;
end;
TQuery = class
public
function Books: TArray<TBook>;
function Authors: TArray<TAuthor>;
function Book(id: Integer): TBook;
function Author(id: Integer): TAuthor;
end;