Previously in this series, I made a GraphQL server that returns nothing. Now I'm going to design the schema (type defintions) and set up mocking functionality. Since I have yet to build the resolvers (which retrieve data from a database or other sources), the server will now return mock data based on the schema.
Overview
I'm building the API for a hypothetical app that displays my favourite music releases. The API server shall return a list of music releases; each list item shall have a title, an artwork image, artist name(s), and URL to play on Spotify.
I borrowed the basic models and descriptions from Spotify Web API official documentation via an app that converts Spotify API’s objects into GraphQL schema I’d built earlier, liberally simplified and modified for this purpose.
TL;DR? View (and fork) the live CodeSandbox app ↓
Part 1: Typing
A schema describes our GraphQL API’s data models, relations, and operations. It is comprised of type definitions written in a syntax called the Schema Definition Language (also called the "GraphQL schema").
So... what exactly are these types that we define? GraphQL recognizes a number of type categories.
- Entry-point types:
Query
andMutation
- Object types
- (Default/built-in) Scalar types
- Custom scalar types
- Enum types
- Input types
- Abstract types: unions and interfaces
We are only using some of those now, but I'm including references at the end of the post if you’d like to learn more.
Quick copypasta of types we're going to use:
# Query type
type Query { ... }
# Object types
type Artist { ... }
type Album { ... }
type Track { ... }
type Image { ... }
# Enum type
enum AlbumType { ... }
# Interface type and object type that implements it
interface Release { ... }
type Album implements Release { ... }
# Union type (we're discussing but _not_ using this)
union Release = Album | Track
Object types
An object type represents a kind of object (...who would’ve thought? 😬) containing a collection of fields (properties) that can be fetched from some data source(s). A GraphQL schema is usually largely comprised of these types.
For our schema, let’s define the object types Artist
, Album
, Track
, and Image
(truncated for brevity).
type Artist {
id: ID!
name: String!
images: [Image!]
}
type Album {
id: ID!
name: String!
artists: [Artist!]!
images: [Image!]
release_date: String
}
type Track {
id: ID!
name: String!
artists: [Artist!]!
album: Album!
track_number: Int
preview_url: String
}
type Image {
url: String!
height: Int
width: Int
}
- An object type must contain at least one field. The object type
Artist
contains fields calledid
,name
, andimages
(and so on). - The field type can be...
- a scalar type (
ID
,String
,Int
—details below), - or another object type (eg. in the
Track
object type, thealbum
field contains anAlbum
object).
- a scalar type (
- The exclamation mark
!
indicates a required field; lack of which indicates nullable. - The square brackets
[]
indicates a list/array of the type inside the brackets.- eg.
[String]
= a list of strings,[Artist]
= a list ofArtist
objects.
- eg.
-
artists: [Artist!]!
means theartists
field is required and cannot be null (right-side/outer exclamation mark). It cannot return an empty array, either; it must contain at least oneArtist
object (inner exclamation mark).
Note that the SDL is agnostic with regards to data sources; we don’t have to define object types and fields in relation to any database table/column structure.
Query type
This is the only required part of a schema. Technically, it’s possible to have a server without a single object type. But we will get an error if the Query
type does not exist.
Query
and Mutation
are GraphQL’s special top-level, entry point types. Clients, ie. front-end apps, can only retrieve (read) data through Query
and store/update/delete (write) data through Mutation
.
Unlike a REST API, a GraphQL API has no multiple endpoints. Clients don't send a GET request to /albums
for a list of albums and another to /artists/:id
for the artist data; they simply send a single query with a list of fields they need, which are the fields in our Query
type.
type Query {
+ albums: [Album!]
+ tracks: [Track!]
}
With our schema above, clients can query for albums
, which will include the relevant artists and images data (if they include those fields in the request). Likewise for tracks
. But they cannot get a list of artists or images, since our type definition does not include such fields.
Like object types and Mutation
, Query
fields can accept arguments.
type Query {
albums: [Album!]
tracks: [Track!]
+ album(id: ID!): Album
+ track(id: ID!): Track
}
The last two fields enable clients to query a single album or a single track by passing an id
as argument.
Note: We are not discussing Mutation
here, but it's essentially Query
's counterpart for POST/PUT/DELETE operations.
Scalar types
Some fields above have types like Int
and String
—these are the default/built-in scalar types, which are GraphQL schema’s primitive types equivalent. There are five scalar types: ID
, String
, Int
, Float
, Boolean
.
We can also define our own custom scalar types. Apollo Server provides the GraphQLScalarType
constructor class to facilitate defining our custom scalar logic, which we then pass to a custom resolver. I'm going to discuss this part in a separate post.
Enum types
Like other typed languages, GraphQL SDL has an enumerated type, a.k.a. enum. An enum field type can only have one of the predefined string values.
The Spotify API’s AlbumObject has an album_type
field, originally typed String
, whose value is one of "album", "single", or "compilation". Let’s define it as an enum type in our schema.
type Album {
id: ID!
name: String!
artists: [Artist!]!
images: [Image!]
release_date: String
+ album_type: AlbumType
}
+ enum AlbumType {
+ ALBUM
+ SINGLE
+ COMPILATION
+ }
In addition to type safety (when querying and displaying on the frontend, when passing as query argument, as well as when making mutations), using enum types also gives us the benefit of intellisense/autocompletion in supported IDEs.
Note that the enum values have to be in UPPERCASE as per GraphQL specs. If the values in the data source (eg. database) are in lowercase, we can map them in a custom resolver function. Not only for mapping cases of course, we can use it eg. to map colour shorthands into corresponding hex values, as shown in the Apollo Server documentation.
Union and interface types
The last types we're going to discuss are the abstract types. Essentially, they represent a type that could be any of multiple object types. Quick definitions by GraphQL Ruby:
- Interface: "a list of fields which may be implemented by object types"
- Union: "a set of object types which may appear in the same spot"
(🤔 ...huh?)
We're going to see how either one could be used in our schema to describe "a music release, which could either be an album or a track".
Option A — with union
One possible approach is by defining a union type called Release
, which could either be an Album
or a Track
object type. Then we add a field called releases
to the query, which returns a list of Release
objects.
+ union Release = Album | Track
type Query {
albums: [Album!]
tracks: [Track!]
+ releases: [Release!]
}
Notes:
- The
Album
andTrack
definitions stay the same. - A union can only consist of two or more object types, not scalar or any other types.
Unexpectedly (to me), this type is not intended for mixed scalar and object types, eg. a field that could be an integer or a particular object type. We need custom scalars for that instead. Related discussions:
Option B — with interface
Alternatively, we can define an interface type called Release
, which is then implemented by the Album
and Track
object types. It contains the fields id
, name
, and artists
, which both Album
and Track
have. Like in option A, we add a field called releases
to the query, which returns a list of Release
objects.
+ interface Release {
+ id: ID!
+ name: String!
+ artists: [Artist!]!
+ }
- type Album {
+ type Album implements Release {
id: ID!
name: String!
artists: [Artist!]!
images: [Image!]
release_date: String
}
- type Track {
+ type Track implements Release {
id: ID!
name: String!
artists: [Artist!]!
album: Album!
track_number: Int
preview_url: String
}
type Query {
albums: [Album!]
tracks: [Track!]
+ releases: [Release!]
}
Another unexpected-to-me finding: We still have to include the common interface fields in the object type fields (in this case id
, name
, artists
). Yes, we write id: ID!
three times and they have to be identical.
So... are they interchangeable?
I ended up in a rabbit hole researching these two types’ difference in usage, but it is an interesting topic! I will probably revisit this in a separate post.
TL;DR + IMO: They are conceptually different, but share similar characteristics as abstract types, which enable them to solve the same problems (such as our Release
type) albeit in different ways.
On a more practical level, when querying, we use inline fragments differently with union and interface types.
A union type does not know what fields each object type does/does not have. So we have to specify all fields inside the inline fragments, including the identical ones (eg. name
and artists
).
query GetUnionReleases {
latest_releases {
... on Album {
# name and artists fields here...
name
artists
images
}
... on Track {
# ...name and artists fields again here
name
artists
album
}
}
}
Meanwhile, with an interface type we can specify the common fields (the interface’s fields) outside the fragments. The Release
interface always contains name
and artists
fields, so we can query them outside the inline fragments.
query GetInterfaceReleases {
latest_releases {
# interface’s fields
name
artists
# object type specific fields in the fragments
... on Album {
images
}
... on Track {
album
}
}
}
These queries will be made from the clients that consume our API, but you can try it from the server app’s GraphQL Playground IDE. For this app, I decided to use the interface type to define Release
(option B).
Half-time
Let’s see our code so far.
// index.js
const { ApolloServer, gql } = require("apollo-server");
const typeDefs = gql`
type Query {
albums: [Album!]
tracks: [Track!]
releases: [Release!]
}
# ... the rest of the schema
`;
const server = new ApolloServer({ typeDefs });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
The server is up and running, and the GraphQL Playground IDE shows the documentation and autocomplete feature based on our type definitions.
We can also use Apollo Studio to access—among other things—a more powerful and dev-friendly query builder.
Above we can see the Release
interface detail, including the types that implement it. When I select the images
field under Album
, it automatically builds the inline fragment in the query. Smooth! 😎
But when we run the query, the server returns null
because we have not set up the resolvers, which will return each field value. Fortunately, Apollo Server has an extensive mocking feature based on our type definitions.
Part 2: Mocking
Mocks out of the box
Add a mocks: true
option when creating a server instance to enable the default mock resolvers.
const server = new ApolloServer({
typeDefs,
mocks: true,
});
When we send the query... the returned data is no longer null
.
Let's query some more data...
query Query {
releases {
id
name
artists {
name
}
... on Album {
album_type
name
release_date
}
... on Track {
name
track_number
explicit
}
}
}
...and here is the returned mock data.
{
"data": {
"releases": [
{
"id": "f4570e2d-9221-4a8e-ae5d-6cf0bcb3c10e",
"name": "Hello World",
"artists": [
{
"name": "Hello World"
},
{
"name": "Hello World"
}
],
"track_number": -65,
"explicit": false
},
{
"id": "be550404-7bad-46c7-a314-cb88ceec1710",
"name": "Hello World",
"artists": [
{
"name": "Hello World"
},
{
"name": "Hello World"
}
],
"album_type": "COMPILATION",
"release_date": "Hello World"
}
]
}
}
-
ID
is mocked as random string uuid. Nice! -
String
is mocked asHello world
. -
Int
is randomly generated. -
Boolean
and enum types are correctly typed! - Array types always return 1-2 items (depending on the number of fields).
- We don't have custom scalar type here. If we did, we would have to pass a custom resolver for the mock to work.
This is pretty cool, considering it takes literally five seconds to implement. But let's make the mock data resemble our expected data better.
Custom mock resolvers
Pass an object to the mocks
option to enable custom mock resolvers. As the name suggests, they are identically shaped to the actual resolvers, both of which correspond to the schema. Each property is a function that returns the corresponding mock value.
For example, this replaces "Hello world" with "My custom mock string" for the String
scalar type value.
- const mocks = true;
+ const mocks = {
+ String: () => "My custom mock string"
+ };
const server = new ApolloServer({
typeDefs,
+ mocks,
});
But the album, track, and artist names are all String
. Does not make much sense if they all return "My custom mock string", does it?
Luckily, we can mock our object types the same way. Remove the String
function and add Album
, Track
, and Artist
functions.
const mocks = {
- String: () => "My custom mock string"
+ Album: () => {
+ return { name: () => "Album Title" };
+ },
+ Track: () => {
+ return { name: () => "Track Title" };
+ },
+ Artist: () => {
+ return { name: () => "Artist Name" };
+ }
};
Let's send another query and see the returned mock data.
query Query {
releases {
__typename
name
artists {
name
}
... on Album {
album_type
}
... on Track {
track_number
}
}
}
{
"data": {
"releases": [
{
"__typename": "Track",
"name": "Track Title",
"artists": [
{
"name": "Artist Name"
},
{
"name": "Artist Name"
}
],
"track_number": 33
},
{
"__typename": "Album",
"name": "Album Title",
"artists": [
{
"name": "Artist Name"
},
{
"name": "Artist Name"
}
],
"album_type": "ALBUM"
}
]
}
}
Note that although we only pass the name
function, the mock logic uses the default mock logic for the remaining fields (eg. track_number
in Track
, album_type
in Album
). Nice!
MockList
Lists—such as releases
and artists
—return two items by default. What if we need different number of items mocked? Apollo Server provides the MockList
constructor to customize the number of items in a list.
const {
ApolloServer,
+ MockList
} = require("apollo-server");
const mocks = {
Album: () => {
- return { name: () => "Album Title" };
+ return { name: () => "Album Title", artists: () => new MockList(1) };
},
Track: () => {
- return { name: () => "Track Title" };
+ return { name: () => "Track Title", artists: () => new MockList(1) };
},
+ Query: () => {
+ return { releases: () => new MockList([0, 15]) };
+ }
}
- Although an album or track can have more than one artists (eg. featuring/collaboration), most albums and tracks just have one artist. Let's add an
artists
mock resolver toAlbum
andTrack
which returns aMockList
object. The integer argument1
means always return one item. - Meanwhile in
Query
, we addreleases
that return between 0 to 15 items, using the array argument[0, 15]
. This is particularly useful for testing and UI development/prototyping—what an empty state looks like, what the pagination looks like, what an "awkward" number of item (eg. just 1) looks like.
Now when we run our query, the returned mock data looks like this.
It's still not ideal (track_number: -60
anyone? 😬), but we've got some mock data to test and start developing the UI/frontend app with.
Putting it together
In this post, we define our API’s types and their relations (Query
, object types, enums, interface) in the GraphQL schema language and pass it as template literal to Apollo Server’s gql
parser. Then we use the built-in mocks
option so our server returns mock data based on our type definitions. We use custom mock resolvers to customize our mock data behaviour.
Stay tuned for the next post in this series, where I'm going to improve my mocks with casual
fake data generator!
Top comments (1)
I wrote about a few practical tips related to Apollo mocking, it's related to this content. Hope you find it useful: dev.to/kimmobrunfeldt/5-tips-for-d...