Migrating to GraphQL on Laravel with Lighthouse

I will be covering how to covert your Laravel Controllers, Resources, and Form Requests into a single GraphQL endpoint. After the conversion you will find that the amount of boilerplate needed per Model will be drastically reduced. My demo consists of a Ticket System and I am using VueJS to power the frontend.

So what is GraphQL?

To quote the GraphQL website,

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.

Basically GraphQL is a layer between your client app and your data. GraphQL is great because you just have to specify the data that can be queried and the client can pick and choose what data to pull. As opposed to a REST API where you define rigid endpoints that return the same amount of data every time. You could hack some parameters into the request to filter data, but it isn’t provided out of the box and may not scale well.

The Setup

I will be using Lighthouse to serve the graphql endpoint. Lighthouse is great because you can use Schema Definition Language, or SDL, to write out your schema. It also supports some great out-of-the-box decorators to make use of Laravel’s features. On the frontend, I will use Vue Apollo to consume the graphql endpoint. Vue Apollo will basically replace Axios and will also act as a local cache for queried data.

Be sure to checkout the docs!

Here is the full code example on github.

Let’s get started!

Installation

We need to install the lighthouse package.

composer require nuwave/lighthouse

Publish the Lighthouse configuration file

php artisan vendor:publish --provider="Nuwave\Lighthouse\Providers\LighthouseServiceProvider" --tag=config

Now you can publish the default schema. This will put a file in the Laravel routes/graphql directory.

php artisan vendor:publish --provider="Nuwave\Lighthouse\Providers\LighthouseServiceProvider" --tag=schema

This schema file is where you define your data that can be queried. A request can be either a query or mutation. A Query simply returns data and doesn’t change anything. A Mutation performs an operation on the data like create, update, and delete. We can define these actions in our schema file. The default schema file looks like this.

There is alot going on here so let’s break it down.

Types

You will see types for Query, Mutation, and User. The root types are the Query and Mutation. Everything must go under either of those root types. The User type is called a user-defined type. These types mostly mirror Laravel models and will be used mostly to define what should be returned when a User model(s) is requested.

Fields

These are the values that are queried by the client. On the Query type we have fields users and user. On the Mutation type the fields are createUser, updateUser, and deleteUser. Each field has a return type and that is specified after the colon. When a return type has an ! behind it that means it is a required field. GraphQL will throw an error if it isn’t present.

Arguments

Fields can accept arguments and those are specific in parenthesis. Each parameter also has a type. Types with the ! are required (like in Fields). Some of the built in Scalar Types are Int, String, Boolean, etc.

Return Types

We can return user-defined types in our Queries and Mutations. Notice how in each Query/Mutation above there is a : User? That means that after every operation the response will return the respective User object to the client.

Directives

The directives are denoted with an @ symbol. I am using the Lighthouse built-in directives and they are pretty self explanatory. It basically allows us to create, update, and delete without any controllers. We just have to supply the required arguments that the directives require. Check out the full list of directives here.

Adding Ticket model to the Schema

Now let’s define our Ticket type in the Schema file.

type Ticket {
id: ID!
title: String!
contact: String!
status: String!
issue: String!
created_at: DateTime!
updated_at: DateTime!
}

This is just all of the fields in the database with a return type. All of the fields are required. Now we need to define the Queries.

type Query {
tickets: [Ticket!]! @all(model: "App\\Ticket")
ticket(id: ID @eq): Ticket @find(model: "App\\Ticket")
}

Here we are just returning all of the available tickets in the tickets query. Then in the ticket query we are returning the ticket that matches the id with @eq.

Here are the mutations

createTicket(
title: String @rules(apply: ["required", "max:255")
contact: String @rules(apply: ["required", "max:255"])
status: String @rules(apply: ["required", "max:255"])
issue: String @rules(apply: ["required", "max:255"])
): Ticket @create(model: "App\\Ticket")
updateTicket(
id: ID @rules(apply: ["required"])
title: String @rules(apply: ["max:255"])
contact: String @rules(apply: ["max:255"])
status: String @rules(apply: ["max:255"])
issue: String @rules(apply: ["max:255"])
): Ticket @update(model: "App\\Ticket")
deleteTicket(
id: ID @rules(apply: ["required"])
): Ticket @delete(model: "App\\Ticket")

Here it is pretty straightforward. The @rules directive uses the Laravel validation and will return an error if validation fails. Here is the full schema file.

And that is it for the Laravel setup! We can now delete the Controllers, Form Requests, Routes, and API Resources.

Vue Apollo Setup

Install the packages.

npm install --save-dev vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag

In your app.js file we need to configure Vue Apollo. I like to create a separate Apollo configuration file and then import it into app.js to keep things clean.

Here is a basic Apollo configuration. This adds the required authentication headers to consume our GraphQL endpoint.

We can then import this configuration in our app.js file. We then need to add the Vue Apollo plugin and specify it in the root Vue instance.

Here is the relevant Apollo configuration.

The Client-side Queries

There are different ways you can set this up. You could hard-code each query into your components, but you will rewrite the same query many times if it is required in multiple components. A better way is to put the queries into separate files and import them into your components. This requires a graphql file loader and we can add this into the Webpack configuration. I am using Laravel Mix in this demo.

Now create a queries folder in the js root to store all of the queries. Create a new file called ticket.gql.

Let’s break this down.

Fragments

I am using fragments to specify the ticket fields that we can query. This isn’t required, but it allows us to specify a list of fields once and use it in multiple queries. This also helps when adding new fields to a type, because we only have to update the fragment.

Queries and Mutations

The Queries and Mutations should line up with the Schema we created earlier. Let’s look at the getTickets query. We first start by specifying if we are creating a Mutation or Query (In this case it is a query). Then we give the query an operation name (getTickets). (Hint: This is the name we will use to import the query into our components). Now we put in the name of the query we specified in the Schema, which is tickets. Since our tickets query returns a user-defined type (Ticket), we need to specify which fields we want to return. We can use the JS Spread operator to put all of the fields in from the fragment.

The rest of the Queries and Mutations have arguments that are required. Arguments are specified in the first set of parenthesis with a Type. Then they are then specified again after the Schema Query/Mutation name. Think of it like you are instantiating the variables and then assigning those variables.

Check out the Official GraphQL documentation. They go over all of this way better than I can.

Adding Apollo to the Ticket Component

We now have all of the leg work done and we can (finally) use the Apollo queries in our components. We can specify Apollo queries by creating an instance property called apollo in components. These queries will run automatically when the component is created. Here is an example of the Tickets.vue component moved to GraphQL.

Imports

Notice that I am importing the needed Queries/Mutations at the top of the script tag.

Template

Your template section should go unchanged during this process. We are using the same data just retrieving it in a different way.

Queries

The variables property within the apollo queries should be an object that matches the arguments. With the getTicket query we are only looking for something like {id: 1}, so we need to return this in the variables section. I also mentioned earlier that there is a caching system built into Apollo. These queries will also save to the cache and will pulled from the cache to reduce API calls.

Mutations

We specify a mutation by using the $apollo helper on the Vue instance. this.$apollo.mutate({}). The mutate function has config options that makes Optimistic Response and cache updates a breeze. An Optimistic Response will provide a fake response with the expected data and will update or rollback if the mutation is successful or not. You can see my use of this in the Update/Add Ticket mutations. Don’t mind the __typename attributes. GraphQL adds and removes these internally, so we just need to add them for the fake response.

Update Cache from Mutation

The update property in the mutation will allow you to manually update the cache. The update function expects the store and the Mutation’s response as arguments. You can use object destructuring to drill right down to the data. Looking at the addTicket mutation, I am loading the tickets array from the cache, appending the newly created ticket, and committing the changes back to the cache. The cache and the data properties are reactive so the table will update automatically.

Protect those routes!

As of right now everything works except our GraphQL endpoint isn’t being protected. Anyone can browse to the Tickets page. We can add the auth:api middleware to the GraphQL endpoint so that we are redirected to the login page if not authenticated.

Open up the lighthouse.php configuration file and find the route property and add auth:api to the middleware array. It should look like this.

'route' => [
'prefix' => '',
'middleware' => ['auth:api']
],

Now we can modify the apollo.js configuration to redirect to the login page on any 401 responses. Do note that this is not spec compliant GraphQL, but it will work for a small app. Here is the updated apollo.js.

This adds a global error handler. The 401 response will be a networkError and we are just looking for a 401 status code. Then we redirect to the login page. Notice we are importing the main Vue instance from the app.js file.

That’s all Folks

That was alot to cover, but I hope that gives you a basic understanding of how GraphQL works and how you can integrate it into your app. I plan to do an in-depth dive into the more advanced features like handling Laravel relationships, custom directives, custom resolvers, and much more.

Make sure you see the full code example on github. If you have any questions, or concerns leave a comment!