« How to build a GraphQL Server using Laravel - Part 3

August 31, 2019 • ☕️ 6 min read

LaravelGraphQLAPIs

asset 1


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-nullable
3 name: String
4 email: String
5 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 field
3 appearsIn # "appearsIn" is a field
4}

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: The ID 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 REST
4 task(id: ID!): Task # REST Equivalent of a GET to `/api/tasks/:id` in REST
5 tasks: [Task!]! # REST Equivalent of a GET to `/api/tasks` in REST
6}

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: Query
3 mutation: Mutation
4}

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 file lighthouse.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 graphql
2touch graphql/schema.graphql

The above commands, creates a directory called graphql and then creates a file named schema.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.graphql
2
3type User {
4 id: ID!
5 name: String!
6 email: String!
7}
8
9type Query {
10 user(id: ID! @eq): User @first
11 users: [User!]! @all
12}

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 id
4 email
5 name
6 }
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 @first
3 users: [User!]! @paginate
4}

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): UserPaginator
3}
4
5type UserPaginator {
6 data: [User!]!
7 paginatorInfo: PaginatorInfo!
8}
9
10type PaginatorInfo {
11 count: Int!
12 currentPage: Int!
13 firstItem: Int
14 hasMorePages: Boolean!
15 lastItem: Int
16 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 total
5 currentPage
6 hasMorePages
7 perPage
8 }
9 data {
10 id
11 email
12 name
13 }
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": 5
9 },
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 id
4 email
5 name
6 }
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.