« How to build a GraphQL Server using Laravel - Part 3
August 31, 2019 • ☕️ 6 min read
In the first part of this series, we understood what GraphQL is, it’s advantages and even went ahead to compare it with REST. In the previous article, we did setup our micro-blog’s model and its database persistence. Now, it’s time to start building out our GraphQL server.
If you haven’t read any of the previous articles, use the links below to catch up: Part 1: What is GraphQL and it’s advantages? GraphQl vs REST , Part 2: Setup our Laravel Project , Part 3: Setup our GraphQL Server & Playground in our project
I hope you are as excited as I was writing this. Just to give you a heads up, currently, there are several packages / libraries that makes it easy to setup a GraphQL server using Laravel, but for this article, we’re going to use Lighthouse.
To keep this tutorial simple, our GraphQL API will only allow us to retrieve the list of Users seeded in our database and also details of specific user.
Terminologies in GraphQL
Before we begin setting up our GraphQL server, let’s first understand a few terminoligies you would come across in “GraphQL World”:
1. Types
Data models in GraphQL are respresented as Types
. They are strongly typed. Every GraphQL server defines a set of types which completely describe the set of possible data you can query on that service. Then, when queries come in, they are validated and executed against that schema.
An example Type
would be:
1type User {2 id: ID! # "!" means required or non-nullable3 name: String4 email: String5 articles: [Article!]! # "[Article]" is another GraphQL type which returns an array of type `Article` objects.6}
Note: There should always be a 1-to-1 mapping between your data models and GraphQL types.
2. Field
A field is a unit of data you can retrieve from an object. According to the official GraphQL docs: “GraphQL is all about asking for specific fields on objects.”
An example Field
would be:
1type Hero {2 name # "name" is a field3 appearsIn # "appearsIn" is a field4}
3. Scalar types
GraphQL comes with a set of default scalar types out of the box as listed below:
Int
: A signed 32‐bit integer.Float
: A signed double-precision floating-point value.String
: A UTF‐8 character sequence.Boolean
: true or false.ID
: TheID
scalar type represents a unique identifier, often used to refetch an object.
4. Queries
Every GraphQL service has a query type. They are special because they define the entry point of every GraphQL query. Queries in GraphQL are similar to SQL Queries, these statements are executed to get data.
By convention, there has to be a root Query
which contains all the queries as below:
1type Query {2 user(id: ID!): User # REST Equivalent of a GET to `/api/users/:id`3 users: [User!]! # REST Equivalent of a GET to `/api/users` in REST4 task(id: ID!): Task # REST Equivalent of a GET to `/api/tasks/:id` in REST5 tasks: [Task!]! # REST Equivalent of a GET to `/api/tasks` in REST6}
5. Mutations
With REST
, a Query is equivalent to a GET
request and Mutations
are equivalent to POST / PUT / PATCH / DELETE
requests. It’s recommended that one doesn’t use GET requests to modify data, GraphQL is similar. A Query
reads data and a Mutation
modifies or writes data.
By convention, we put all our mutations in a root Mutation
as shown below:
1type Mutation {2 createUser(3 name: String!4 email: String!5 password: String!6 ): User # REST Equivalent of a POST `/api/users`7 updateUser(8 id: ID!9 name: String!10 email: String!11 password: String!12 ): User # REST Equivalent of a PATCH `/api/users`13 deleteUser(14 id: ID!15 ): User # REST Equivalent of a DELETE `/api/users`16}
6. Schema
Because the shape of a GraphQL query closely matches it’s results, you can predict what the query will return without knowing that much about the server. But it’s useful to have an exact description of the data we can ask for - what fields can we select? What kinds of objects might they return? What fields are available on those sub-objects?
That’s where the schema comes in. Schemas describe how data are shaped and what data on the server can be queried. Simply put, the schema is what the GraphQL endpoint exposes to the world. A GraphQL API endpoint provides a complete description of what a client can query. Schemas can be of two types, that is, Query and Mutation as seen below :
1schema {2 query: Query3 mutation: Mutation4}
The schema is strongly typed and this enables the autocomplete feature in GraphiQL / GraphQL Playground (The GraphQL API Interactive Interface)
7. Resolvers
Each field on a Type is backed by a function called a Resolver
. When a Field is executed, it’s corresponding Resolver
is called. Basically, Resolvers
are the muscles behind GraphQL, they do the heavy lifting. They can;
- Call a microservice
- Hit the database layer to perform CRUD operations
- Call and internal REST endpoint
- Call a method of a class in your application
—
Setting up Lighthouse
To support GraphQL in our application we need to install a library that allows you to define schemas, queries and mutations in a simple way, hence, we install Lighthouse.
Remember, GraphQL is a specification. This means that GraphQL is independent of any programming language.
If you want to use it in your application, you need to choose among the several available implementations available in almost any language.
To install lighthouse, run the following commands from the project root:
1. Install via composer :
1composer require nuwave/lighthouse
2. Publish Lighthouse’s configuration file :
Removing --tag=config
option would publish Lighthouse’s default Schema file. In this article, we would create our schema file from scratch.
1php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=config
Note: A look inside our project
config
directory will reveal the filelighthouse.php
which contains all the configurations of lighthouse.
Defining our Schema
With the setup out of the way, let’s start defining the schema for our application. Note, Lighthouse’s default Schema file was not published because we added the option --tag=config
when publishing it’s configuration files hence the need to create our own Schema.
To create our Schema file run the commands below in the root of our project;
1mkdir graphql2touch graphql/schema.graphql
The above commands, creates a directory called
graphql
and then creates a file namedschema.graphql
inside this directoy.
We created and named the file schema.graphql
because if you take a look at the schema array in config/lighthouse.php
, you’ll notice a setting used to register our schema file with Lighthouse specifies the file path and name:
1'schema' => [2 'register' => base_path('graphql/schema.graphql'),3],
Next we set up our user object type and query as seen below:
1// graphql/schema.graphql23type User {4 id: ID!5 name: String!6 email: String!7}89type Query {10 user(id: ID! @eq): User @first11 users: [User!]! @all12}
From the above, you might have noticed see a few identifiers in the root query such as @eq
, @first
and @all
. These identifies are called Schema Directives and you can read more about them here. You can also find a full reference of Lighthouse’s directives here
From the above code, our User
object type which has a 1-to-1 relation to our data model App\User
has it’s fields defined as id
, name
and email
. From this, we can deduce that the columns password
, created_at
and updated_at
of our data model cannot be queried from our GraphQL endpoint.
Furthermore, our entry point into our API which is the root Query type, defines it’s first field as user
. This field takes an ID
as an argument and returns a single User
object type. This same field has two directives @eq
and @first
, which tells Lighthouse to only return a result when the ID
passed as an argument matches an id
in our database, and @first
also instructs Lighthouse to return the first results. An example query using Laravel’s query builder will be:
1App\User::where('id', $request->input('id'))->first();
Moreover, the second field in our query type called users
, returns an array of User
object types. The directive @all
on the field tells Lighthouse to retrieve all users using our User
model. A similar query using Laravel’s query builder will be:
1App\User::all();
Setup our GraphQL Playground
To enable us test our GraphQL API, we have to install our GraphQL Playground. This playground allows us to query our GraphQL endpoint and provides us with all the benefits such as autocomplete, error highlightling, documentation, etc. However, you may use a standard client such as Postman or run cURL command in terminal but would loose all the benefits the playground provides.
To install the playground, run the command below in your terminal:
1composer require mll-lab/laravel-graphql-playground
Next, let’s publish the configuration and views of our GraphQL playground by running the command below:
1php artisan vendor:publish --provider="MLL\GraphQLPlayground\GraphQLPlaygroundServiceProvider"
Testing our GraphQL API
Now let’s run the server and begin querying our data with the command below:
1php artisan serve
Note: By default, the endpoint lives at
/graphql
and the playground is accessible at/graphql-playground
. Note our graphql playground always assumes a running GraphQL endpoint at/graphql
.
On the left side of the graphql playground, we can query for all users seeded in the database as defined in our schema by running the query below:
1query {2 users {3 id4 email5 name6 }7}
When you hit the play button in the middle of the playground you’ll see the JSON
output of our on it’s right side with a response like this:
1{2 "data": {3 "users": [4 {5 "id": "1",6 "email": "schneider.august@example.org",7 "name": "Rowland Schmeler Sr."8 },9 {10 "id": "2",11 "email": "kale.bernier@example.net",12 "name": "Raegan Schultz"13 },14 {15 "id": "3",16 "email": "guiseppe.altenwerth@example.org",17 "name": "Mozell Ankunding"18 },19 {20 "id": "4",21 "email": "udeckow@example.com",22 "name": "Murray Cruickshank"23 },24 {25 "id": "5",26 "email": "flatley.kimberly@example.org",27 "name": "Reid Douglas"28 },29 {30 "id": "6",31 "email": "volkman.nelda@example.net",32 "name": "Verna Cummerata"33 },34 {35 "id": "7",36 "email": "xzavier28@example.com",37 "name": "Filiberto Stamm"38 },39 {40 "id": "8",41 "email": "prosacco.missouri@example.com",42 "name": "Bette Keeling"43 },44 {45 "id": "9",46 "email": "rfeeney@example.org",47 "name": "Dr. Junius Botsford MD"48 },49 {50 "id": "10",51 "email": "lmorissette@example.com",52 "name": "Gisselle Rodriguez"53 }54 ]55 }56}
Note: In the example above my response is shortend to only 10 results
Paginating our API Response
In a real world project, it’s highly unlikely you will want to return all the users in your database especially if you have hundreds of thousands of users. Yes, we will need to implement pagination.
If you take a look at the full reference of Lighthouse directive you would notice we have a @paginate
directive readibly available to us to enable pagination.
Now let’s update our schema’s query object by replacing @all
with the @paginate
directive:
1type Query {2 user(id: ID! @eq): User @first3 users: [User!]! @paginate4}
Note: Lighthouse caches the schema file, therefore remember to clear it by running the command
php artisan lighthouse:clear-cache
everytime you udpate the Schema file.
Should you run the query to retrieve list of all users you should see an error message like:
Cannot query field\"id\" on type \"UserPaginator\".
I am sure you are asking yourself, why that error after adding the @paginate
directive? Lighthouse behind the scenes changed the return type of the users
field to get us a paginated set of results. Looking at the error message you can deduce the user
field now returns an object of type UserPaginator
.
Below is the transformed schema definition after pagination:
1type Query {2 posts(first: Int!, page: Int): UserPaginator3}45type UserPaginator {6 data: [User!]!7 paginatorInfo: PaginatorInfo!8}910type PaginatorInfo {11 count: Int!12 currentPage: Int!13 firstItem: Int14 hasMorePages: Boolean!15 lastItem: Int16 lastPage: Int!17 perPage: Int!18 total: Int!19}
Note: You do not need to add this to your Schema file, lighthouse already handles this transformation and returns the above object type with it’s related fields.
Now, our query to retrieve a list of users would change since lighthouse is transforming it behind the scenes. Our query would now look like this:
1query {2 users(first:5, page:1) {3 paginatorInfo {4 total5 currentPage6 hasMorePages7 perPage8 }9 data {10 id11 email12 name13 }14 }15}
And our results would look like:
1{2 "data": {3 "users": {4 "paginatorInfo": {5 "total": 51,6 "currentPage": 1,7 "hasMorePages": true,8 "perPage": 59 },10 "data": [11 {12 "id": "1",13 "email": "schneider.august@example.org",14 "name": "Rowland Schmeler Sr."15 },16 {17 "id": "2",18 "email": "kale.bernier@example.net",19 "name": "Raegan Schultz"20 },21 {22 "id": "3",23 "email": "guiseppe.altenwerth@example.org",24 "name": "Mozell Ankunding"25 },26 {27 "id": "4",28 "email": "udeckow@example.com",29 "name": "Murray Cruickshank"30 },31 {32 "id": "5",33 "email": "flatley.kimberly@example.org",34 "name": "Reid Douglas"35 }36 ]37 }38 }39}
Retrieving a Specific User
Now let’s try querying for a specific user details with the id
of 10:
1query {2 user(id: 10) {3 id4 email5 name6 }7}
And we’ll get the following output as the response of the query:
1{2 "data": {3 "user": {4 "id": "10",5 "email": "lmorissette@example.com",6 "name": "Gisselle Rodriguez"7 }8 }9}
Conclusion
You would find the entire source code of this series here. In that repository, i go a step further to use mutations and even setup authentication.
I hope you enjoyed the entire series. Our aim was to understand GraphQL, build something with it so we could have a taste of it and it’s enormous benefits. GraphQL might be new but I strongly believe is the future of APIs.
Feel free to hit me up with your views, comments or questions.