Getting Started
In this chapter, you will learn about the GraphQL schema:
- How it performs as an API contract between the consumer and the provider
- How you can use
graphql
library as a basic GraphQL execution mechanism - What is a GraphQL operation and how you can use it?
Getting Started with GraphQL
To get a better understanding of how GraphQL works, you can start by reading this tutorial about GraphQL basics.
If you are already familiar with the basics of GraphQL, here's a quick overview:
- The GraphQL schema is where your GraphQL types are defined.
- GraphQL's types are connected using fields, and they form a graph.
- The
Query
,Mutation
andSubscription
types are special since they act as an entry point to the graph. - The GraphQL schema acts as the data provider, and it offers a set of capabilities the consumer can use.
- To get data from a GraphQL schema, you need to write a GraphQL operation (often referred to as
query
) that selects the data and fields you need.
In this section of the tutorial, you'll write a simple GraphQL schema, and you'll consume it directly, just for the learning process.
Later, you'll replace the direct execution with a GraphQL server (based on HTTP protocol), and you'll add developer tools that will make it super simple to query and access.
Creating your First GraphQL Schema
There are many ways to create a GraphQL schema - in this tutorial, you are going to use the schema-first approach, and build the schema with the @graphql-tools/schema
library (take a look at the end of this chapter for different solutions for creating your GraphQL schema).
Start by installing graphql
and @graphql-tools/schema
library in your project, using the following command:
yarn add @graphql-tools/schema graphql
The command above will get you the following libraries installed in the project:
graphql
is the GraphQL engine implementation.@graphql-tools/schema
is a library for creating GraphQL executable schemas.
A GraphQL schema can be written with GraphQL SDL (Schema Definition Language), which is the GraphQL language for defining your API/contract. The actual code and business logic of each field is called a GraphQL resolver.
So let's get started by creating your first, very-simple, GraphQL schema.
To get started with a simple GraphQL schema, you need to create a SDL file defining our contract:
Create a src/schema.ts
file with the following content:
const typeDefinitions = /* GraphQL */ `
type Query {
hello: String!
}
`
These are our type definitions that describe what data can be retrieved from the schema.
Up next you define the corresponding resolver functions that are used for resolving the data.
const typeDefinitions = /* GraphQL */ `
type Query {
hello: String!
}
`
const resolvers = {
Query: {
hello: () => 'Hello World!'
}
}
As you can see the resolver map follows the same structure as the type definitions.
The typename (Query
) is an object in which the fields (hello
) are functions that return the corresponding data of that field.
Now you still need to glue together the type definitions and the resolver map.
import { makeExecutableSchema } from '@graphql-tools/schema'
const typeDefinitions = /* GraphQL */ `
type Query {
hello: String!
}
`
const resolvers = {
Query: {
hello: () => 'Hello World!'
}
}
export const schema = makeExecutableSchema({
resolvers: [resolvers],
typeDefs: [typeDefinitions]
})
In the code snippet above, you've created or used the following variables:
typeDefinitions
- this is your GraphQL schema definition. You've created aQuery
type that exposes a field calledhello
, of typeString
resolvers
- the resolver functions are part of the GraphQL schema, and they are the actual implementation (code/logic) of the GraphQL schemaschema
- a combination of the GraphQL SDL and the resolvers.makeExecutableSchema
function is in charge of gluing them together into an executable schema we can later use
Now that you have a GraphQL schema, you can use that to fetch data using a GraphQL query
operation!
Query the GraphQL schema
As explained before, the GraphQL schema is only your contract, and it exposes the set of all types and capabilities that your API layer can do.
To use your GraphQL schema and consume data from it, you will need to write a GraphQL query
operation.
Based on the schema you created before, you can use the following query:
query {
hello
}
So you don't have to get into all the complexity of running a GraphQL server - you can use this query and run it against your GraphQL schema and get an immediate result.
To query our local schema, even without any fancy GraphQL client or even a GraphQL server, you can use GraphQL's execute
function to just run the schema with the query.
Update the code in src/main.ts
to contain the following snippet:
import { execute, parse } from 'graphql'
import { schema } from './schema'
async function main() {
const myQuery = parse(/* GraphQL */ `
query {
hello
}
`)
const result = await execute({
schema,
document: myQuery
})
console.log(result)
}
main()
Now, try to run our project again (either with npm run dev
or npm run start
), you should see in the output log the following:
{ data: [Object: null prototype] { hello: 'Hello World' } }
You can ignore the [Object: null prototype]
. It is just means that the object does not inherit any properties from the global Object prototype and is a security measure that is insignificant for this tutorial.
So What Happened Here?
First, the GraphQL query
operation string was parsed it using the parse
function of graphql
- this will create a DocumentNode
object that can later be executed by GraphQL.
The DocumentNode
is an abstract syntax tree (AST), an intermediate format that represents the operation string in a more processable friendly way for machines.
The JSON representation of the DocumentNode
of the query { hello }
operation looks like the following.
{
"kind": "Document",
"definitions": [
{
"kind": "OperationDefinition",
"operation": "query",
"variableDefinitions": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"name": {
"kind": "Name",
"value": "hello"
},
"arguments": []
}
]
}
}
]
}
Then, the execute
function of graphql
was called with the following parameters:
schema
- this is the GraphQL schema object you previously created.myQuery
- this is theDocumentNode
object created based on our GraphQL query.
The return value of execute
is the GraphQL result (or, GraphQL response).
The GraphQL engine takes the query, and based on the fields you selected (called the Selection-Set), it runs the resolvers and returns their return value.
The next chapter will teach you how to use your GraphQL schema to create a GraphQL server!
A Word on the GraphQL Schema
At the core of every GraphQL API, there is a GraphQL schema. So, let's quickly talk about it.
Note: In this tutorial, we'll only scratch the surface of this topic. If you want to go a bit more in-depth and learn more about the GraphQL schema as well as its role in a GraphQL API, be sure to check out this excellent article.
GraphQL's schemas are usually written in the GraphQL Schema Definition Language (SDL). SDL has a type system that allows you to define data structures (just like other strongly typed programming languages such as Java, TypeScript, Swift, Go, etc.).
How does that help in defining the API for a GraphQL server, though? Every GraphQL schema has three special root
types: Query
, Mutation
, and Subscription
. The root types correspond to the three operation types offered by
GraphQL: queries, mutations, and subscriptions. The fields on these root types are called root fields and define the
available API operations.
As an example, consider the simple GraphQL schema we used above:
type Query {
hello: String!
}
This schema only has a single root field, called hello
. When sending queries, mutations or subscriptions to a GraphQL
API, these always need to start with a root field! In this case, we only have one root field, so there's only one
possible query that's accepted by the API.
Let's now consider a slightly more advanced example:
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!): User!
}
type User {
id: ID!
name: String!
}
In this case, we have three root fields: users
and user
on Query
as well as createUser
on Mutation
.
An additional definition of the User
type is required because otherwise, the schema definition would be incomplete.
What are the API operations that can be derived from this schema definition? Well, we know that each API operation
always needs to start with a root field. However, we haven't learned yet what it looks like when the type of root
field is itself another object type. This is the case here,
where the types of the root fields are [User!]!
, User
and User!
. In the hello
example from before, the type of
the root field was a String
, which is a scalar type.
When the type of root field is an object type, you can further expand the query (or mutation/subscription) with fields of that object type.
Here are the operations that are accepted by a GraphQL API that implements the above schema:
# Query for all users
query {
users {
id
name
}
}
# Query a single user by their id
query {
user(id: "user-1") {
id
name
}
}
# Query multiple users and a single user by their id
query {
users {
id
name
}
user(id: "user-1") {
id
name
}
}
# Create a new user
mutation {
createUser(name: "Bob") {
id
name
}
}
There are a few things to note:
- In these examples, we always query
id
andname
of the returnedUser
objects. We could potentially omit either of them. Note, however, that when querying an object type, it is required that you query at least one of its fields in a selection set. - For the fields in the selection set, it doesn't matter whether the type of the root field is required or a list.
In the example schema above, the three root fields all have different
type modifiers (i.e. different combinations of being a list
and/or required) for the
User
type:- For the
users
field, the return type[User!]!
means it returns a list (which itself cannot benull
) ofUser
elements. The list can also not contain elements that arenull
. So, you're always guaranteed to either receive an empty list or a list that only contains non-nullUser
objects. - For the
user(id: ID!)
field, the return typeUser
means the returned value could benull
or aUser
object. - For the
createUser(name: String!)
field, the return typeUser!
means this operation always returns aUser
object.
- For the
You will learn how to write the corresponding resolver function maps for addition type fields in the following chapters.
Additional Resources
As mentioned at the beginning of this chapter, there are many ways to build and create your GraphQL schema.
Here are a few popular open-source libraries:
graphql
- you can use rawgraphql
using object classes to create your schema@graphql-tools/schema
- schema-first library for creating executable schemaspothos
- library for creating GraphQL schemas in typescript using a strongly typed code first approach and a built-in plugin systemgqtx
- library for creating GraphQL schemas in typescript using a strongly typed code first approachnexus-graphql
- Declarative, Code-First GraphQL Schemas for JavaScript/TypeScripttypegraphql
- GraphQL schema and resolvers with TypeScript, using classes and decoratorsgraphql-modules
- schema first library for creating strict, reusable GraphQL schema modules