Today, we’ll be diving deeper into working with GraphQL and Laravel by building an API with GraphQL and Laravel. This will cover things like authentication, querying nested resources and eager loading related models.
Prerequisites
This tutorial assumes the following:
- Basic knowledge of GraphQL
- Basic knowledge of Laravel
Also, ensure you have the following installed:
- PHP
- Composer
- SQLite
What we’ll be building
We’ll be building an API for a microblogging platform with which users can share small code snippets. Users will be able to edit and delete their own snippets. Also, users will be able to reply to and like snippets posted by other users. Some of these features will be restricted to only authenticated users.
Getting started
To save us some time, I have created a basic Laravel project with the laravel-graphql and the jwt-auth packages installed and set up. The jwt-auth
package will allow us to use JSON Web Tokens (JWT) for authentication.
Let’s clone it from GitHub:
$ git clone -b start https://github.com/ammezie/codebits-api.git
Once that’s done, let’s install the project’s dependencies:
cd codebits-api
$ composer install
Rename .env.example
to .env
. Lastly, run the commands below to generate the app key and JWT secret respectively:
$ php artisan key:generate
$ php artisan jwt:secret
Create models and migrations
In addition to the User model that comes with Laravel by default, we’ll be creating three more models and their corresponding migration files: Bit, Reply, Like.
$ php artisan make:model Bit -m
$ php artisan make:model Reply -m
$ php artisan make:model Like -m
Next, let’s open the migration file generated for the Bit model and update the up
method as below:
// database/migrations/TIMESTAMP_create_bits_table.php
public function up()
{
Schema::create('bits', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->text('snippet');
$table->timestamps();
});
}
We’ll do the same for both the Like and Reply migration files respectively:
// database/migrations/TIMESTAMP_create_likes_table.php
public function up()
{
Schema::create('likes', function (Blueprint $table) {
$table->unsignedInteger('user_id');
$table->unsignedInteger('bit_id');
});
}
// database/migrations/TIMESTAMP_create_replies_table.php
public function up()
{
Schema::create('replies', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->unsignedInteger('bit_id');
$table->string('reply');
$table->timestamps();
});
}
Before we run the migrations, let’s set up our database. We’ll be using SQLite. So, create a database.sqlite
file inside the database
directory then update the .env
file as below:
// .env
DB_CONNECTION=sqlite
DB_DATABASE=ABSOLUTE_PATH_TO_DATABASE_SQLITE_FILE
Run the migrations:
$ php artisan migrate
You will notice the likes
table doesn’t have the timestamps (created_at
and updated_at
) fields. So we need to make Eloquent aware we are not using timestamps for this particular table. To do that, add the line below to the Like model:
// app/Like.php
public $timestamps = false;
Define relationships between models
Let’s define the relationships between our models. We’ll be defining only the necessary parts of the relationships needed for the purpose of this tutorial. We’ll start by defining the relationship between a user and a bit, which will be a one-to-many relationship. That is, a user can add as many bits as they wish, but a bit can only belong to one user. Add the code below inside the User model (that is, the User class):
// app/User.php
public function bits()
{
return $this->hasMany(Bit::class);
}
Next, let’s define the inverse of the relationship on the Bit model (that is, the Bit class):
// app/Bit.php
public function user()
{
return $this->belongsTo(User::class);
}
Users can leave replies on a bit, hence a bit can have many replies. A reply can only belong to one bit. This is also a one-to-many relationship. Add the code below to the Bit model (that is, the Bit class):
// app/Bit.php
public function replies()
{
return $this->hasMany(Reply::class);
}
Let’s define the inverse of the relationship on the Reply model (that is, the Reply class):
// app/Reply.php
public function bit()
{
return $this->belongsTo(Bit::class);
}
In the same vein, a bit can have many likes. Add the code below to the Bit model (that is, the Bit class):
// app/Bit.php
public function likes()
{
return $this->hasMany(Like::class);
}
Lastly, a reply can be made by a user. Add the code below to the Reply model (that is, the Reply class):
// app/Reply.php
public function user()
{
return $this->belongsTo(User::class);
}
Create GraphQL types
We’ll be creating GraphQL types corresponding to our models with the exception of the Like model, which we won’t be creating a type for. Create a new GraphQL
directory within the app
directory. Within the GraphQL
directory, create a new Type
directory. Within app/GraphQL/Type
directory, create a new UserType.php
file and paste the following code in it:
// app/GraphQL/Type/UserType.php
<?php
namespace App\GraphQL\Type;
use GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;
class UserType extends GraphQLType
{
protected $attributes = [
'name' => 'User',
'description' => 'A user'
];
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'The id of a user'
],
'name' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The name of a user'
],
'email' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The email address of a user'
],
'bits' => [
'type' => Type::listOf(GraphQL::type('Bit')),
'description' => 'The user bits'
],
'created_at' => [
'type' => Type::string(),
'description' => 'Date a was created'
],
'updated_at' => [
'type' => Type::string(),
'description' => 'Date a was updated'
],
];
}
protected function resolveCreatedAtField($root, $args)
{
return (string) $root->created_at;
}
protected function resolveUpdatedAtField($root, $args)
{
return (string) $root->updated_at;
}
}
The UserType
contains the exact same fields corresponding to those in the users
tables. It also contains an additional bits
field, which is of BitType
(we’ll create this shortly). This will allow us to retrieve the bits (using the relationship defined above) that a user has posted. Lastly, because Laravel will automatically cast the created_at
and the updated_at
fields to an instance of carbon, we need to specifically define how we want these fields to be resolved. So we define a resolve function for each of the fields by using the convention resolve[FIELD_NAME]Field
. As you can see, we are simply casting the fields to string.
Now, let’s create the BitType
. Within app/GraphQL/Type
directory, create a new BitType.php
file and paste the following code in it:
// app/GraphQL/Type/BitType.php
<?php
namespace App\GraphQL\Type;
use GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;
class BitType extends GraphQLType
{
protected $attributes = [
'name' => 'Bit',
'description' => 'Code bit'
];
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'The id of a bit'
],
'user' => [
'type' => Type::nonNull(GraphQL::type('User')),
'description' => 'The user that posted a bit'
],
'snippet' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The code bit'
],
'created_at' => [
'type' => Type::string(),
'description' => 'Date a bit was created'
],
'updated_at' => [
'type' => Type::string(),
'description' => 'Date a bit was updated'
],
'replies' => [
'type' => Type::listOf(GraphQL::type('Reply')),
'description' => 'The replies to a bit'
],
'likes_count' => [
'type' => Type::int(),
'description' => 'The number of likes on a bit'
],
];
}
protected function resolveCreatedAtField($root, $args)
{
return (string) $root->created_at;
}
protected function resolveUpdatedAtField($root, $args)
{
return (string) $root->updated_at;
}
protected function resolveLikesCountField($root, $args)
{
return $root->likes->count();
}
}
Same as we did with the UserType
. We define additional fields (user
, replies
, and likes_count
). The user
field is of the UserType
, which will be used to retrieve the user (using the relationship defined above) that posted the bit. Also, the replies
field is of ReplyType
(we’ll create this shortly), which will be used to retrieve the replies (using the relationship defined above) that have been left on the bit. The likes_count
field will be used to retrieve the number of likes the bit has. We define how the likes_count
field will be resolved. Again, we use the relationship defined above to retrieve the bit likes and return the count.
Let’s create our last type, which will be ReplyType
. Within app/GraphQL/Type
directory, create a new ReplyType.php
file and paste the following code in it:
// app/GraphQL/Type/ReplyType.php
<?php
namespace App\GraphQL\Type;
use GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;
class ReplyType extends GraphQLType
{
protected $attributes = [
'name' => 'Reply',
'description' => 'Reply to codebit'
];
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'The id of a reply'
],
'user' => [
'type' => Type::nonNull(GraphQL::type('User')),
'description' => 'The user that posted a reply'
],
'bit' => [
'type' => Type::nonNull(GraphQL::type('Bit')),
'description' => 'The bit that was replied to'
],
'reply' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The reply'
],
'created_at' => [
'type' => Type::string(),
'description' => 'Date a bit was created'
],
'updated_at' => [
'type' => Type::string(),
'description' => 'Date a bit was updated'
],
];
}
protected function resolveCreatedAtField($root, $args)
{
return (string) $root->created_at;
}
protected function resolveUpdatedAtField($root, $args)
{
return (string) $root->updated_at;
}
}
This follows the same approach as the other types, with additional fields (user
and bit
) to retrieve the user that left the reply and the bit the reply was left on respectively.
Now, let’s make GraphQL aware of our types by adding them to config/graphql.php
:
// config/graphql.php
'types' => [
'User' => \App\GraphQL\Type\UserType::class,
'Bit' => \App\GraphQL\Type\BitType::class,
'Reply' => \App\GraphQL\Type\ReplyType::class,
],
User sign up
With our types created, let’s allow users to sign up. For this, we’ll create a mutation, which we’ll call SignUpMutation
. Create a new Mutation
directory within app/GraphQL
, and within Mutation
, create a new SignUpMutation.php
file and paste the following code into it:
// app/GraphQL/Mutation/SignUpMutation.php
<?php
namespace App\GraphQL\Mutation;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
use App\User;
class SignUpMutation extends Mutation
{
protected $attributes = [
'name' => 'signUp'
];
public function type()
{
return Type::string();
}
public function args()
{
return [
'name' => [
'name' => 'name',
'type' => Type::nonNull(Type::string()),
'rules' => ['required'],
],
'email' => [
'name' => 'email',
'type' => Type::nonNull(Type::string()),
'rules' => ['required', 'email', 'unique:users'],
],
'password' => [
'name' => 'password',
'type' => Type::nonNull(Type::string()),
'rules' => ['required'],
],
];
}
public function resolve($root, $args)
{
$user = User::create([
'name' => $args['name'],
'email' => $args['email'],
'password' => bcrypt($args['password']),
]);
// generate token for user and return the token
return auth()->login($user);
}
}
We define the type this mutation will return, then define an args
method that returns an array of arguments that the mutation can accept. We also define some validation rules for each of the argument. The resolve
method handles the actual creating of the user and persisting into the database. Lastly, we log the new user in by generating a token (JWT), then we return the token.
Next, let’s add the mutation to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
// ...
'mutation' => [
'signUp' => \App\GraphQL\Mutation\SignUpMutation::class,
]
]
],
User log in
Returning users should be able to log in, let’s create a LogInMutation
for that. Create a new LogInMutation.php
file within app/GraphQL/Mutation
and paste the following code into it:
// app/GraphQL/Mutation/LogInMutation.php
<?php
namespace App\GraphQL\Mutation;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
class LogInMutation extends Mutation
{
protected $attributes = [
'name' => 'logIn'
];
public function type()
{
return Type::string();
}
public function args()
{
return [
'email' => [
'name' => 'email',
'type' => Type::nonNull(Type::string()),
'rules' => ['required', 'email'],
],
'password' => [
'name' => 'password',
'type' => Type::nonNull(Type::string()),
'rules' => ['required'],
],
];
}
public function resolve($root, $args)
{
$credentials = [
'email' => $args['email'],
'password' => $args['password']
];
$token = auth()->attempt($credentials);
if (!$token) {
throw new \Exception('Unauthorized!');
}
return $token;
}
}
This is similar to the SignUpMutation
. It accepts the user email address and password. Using these credentials, it attempts to log the user in. If the credentials are valid, a token is generated, which is in turn returned. Otherwise, we throw an exception.
Next, add the mutation to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
// ...
'mutation' => [
...,
'logIn' => \App\GraphQL\Mutation\LogInMutation::class,
]
]
],
Posting a new bit
Once a user is authenticated, the user will be able to post a new bit. Let’s create the mutation for posting a new bit. Within app/GraphQL/Mutation
, create a new NewBitMutation.php
file and paste the following code into it:
// app/GraphQL/Mutation/NewBitMutation.php
<?php
namespace App\GraphQL\Mutation;
use GraphQL;
use App\Bit;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
class NewBitMutation extends Mutation
{
protected $attributes = [
'name' => 'newBit'
];
public function type()
{
return GraphQL::type('Bit');
}
public function args()
{
return [
'snippet' => [
'name' => 'snippet',
'type' => Type::nonNull(Type::string()),
'rules' => ['required'],
],
];
}
public function authenticated($root, $args, $currentUser)
{
return !!$currentUser;
}
public function resolve($root, $args)
{
$bit = new Bit();
$bit->user_id = auth()->user()->id;
$bit->snippet = $args['snippet'];
$bit->save();
return $bit;
}
}
This mutation returns a BitType
and accepts just one argument, which is the code snippet – this is required. Since only authenticated users can post a new bit, we define an authenticated
method whose third parameter will be the currently authenticated user or null
if a user is not logged in. So we simply cast whatever currentUser
holds to boolean. That is, the authenticated
method will return true
if the user is authenticated and hence proceed with the rest of the mutation. If the method returns false
, the mutation will throw an “Unauthenticated” error message. In the resolve
method, we create a new bit using the ID of the currently authenticated user and the snippet supplied, then persist it to the database. Finally, we return the newly created bit.
Next, add the mutation to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
// ...
'mutation' => [
...,
'newBit' => \App\GraphQL\Mutation\NewBitMutation::class,
]
]
],
Fetching all bits
Let’s move on to creating our first query. This query will be used to fetch all the bits that have been posted. Create a new Query
directory inside app/GraphQL
, and within the Query
directory, create a new AllBitsQuery.php
file and paste the following code in it:
// app/GraphQL/Query/AllBitsQuery.php
<?php
namespace App\GraphQL\Query;
use GraphQL;
use App\Bit;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Query;
use GraphQL\Type\Definition\ResolveInfo;
class AllBitsQuery extends Query
{
protected $attributes = [
'name' => 'allBits'
];
public function type()
{
return Type::listOf(GraphQL::type('Bit'));
}
public function resolve($root, $args, $context, ResolveInfo $info)
{
$fields = $info->getFieldSelection();
$bits = Bit::query();
foreach ($fields as $field => $keys) {
if ($field === 'user') {
$bits->with('user');
}
if ($field === 'replies') {
$bits->with('replies');
}
if ($field === 'likes_count') {
$bits->with('likes');
}
}
return $bits->latest()->get();
}
}
We define the query type to be a list of the BitType
. Then we define the resolve
method, which will handle the actual fetching of the bits. We use a getFieldSelection
method to get the names of all fields selected when the query is run. Using these fields, we eager load the respective related models then return the bits from the most recently added.
Let’s add the query to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
'query' => [
'allBits' => \App\GraphQL\Query\AllBitsQuery::class,
],
// ...
]
],
Fetch a single bit
Let’s add the query to fetch a single bit by it ID. Within app/GraphQL/Query
, create a new BitByIdQuery.php
file and paste the following code into it:
// app/GraphQL/Query/BitByIdQuery.php
<?php
namespace App\GraphQL\Query;
use GraphQL;
use App\Bit;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Query;
class BitByIdQuery extends Query
{
protected $attributes = [
'name' => 'bitById'
];
public function type()
{
return GraphQL::type('Bit');
}
public function args()
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required']
],
];
}
public function resolve($root, $args)
{
if (!$bit = Bit::find($args['id'])) {
throw new \Exception('Resource not found');
}
return $bit;
}
}
This query accepts a single argument, which is required. In the resolve
method, we fetch a bit whose ID matches the id
argument supplied. If none is found, we throw an exception. Otherwise, we return the bit.
Add the query to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
'query' => [
'bitById' => \App\GraphQL\Query\BitByIdQuery::class,
],
// ...
]
],
Reply to a bit
Within app/GraphQL/Mutation
, create a new ReplyBitMutation.php
file and paste the following code into it:
// app/GraphQL/Mutation/ReplyBitMutation.php
<?php
namespace App\GraphQL\Mutation;
use GraphQL;
use App\Bit;
use App\Reply;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
class ReplyBitMutation extends Mutation
{
protected $attributes = [
'name' => 'replyBit'
];
public function type()
{
return GraphQL::type('Reply');
}
public function args()
{
return [
'bit_id' => [
'name' => 'bit_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required'],
],
'reply' => [
'name' => 'reply',
'type' => Type::nonNull(Type::string()),
'rules' => ['required'],
],
];
}
public function authenticated($root, $args, $currentUser)
{
return !!$currentUser;
}
public function resolve($root, $args)
{
$bit = Bit::find($args['bit_id']);
$reply = new Reply();
$reply->user_id = auth()->user()->id;
$reply->reply = $args['reply'];
$bit->replies()->save($reply);
return $reply;
}
}
This returns a ReplyType
and accepts two arguments: the ID of the bit the reply is for and the reply content. It is also restricted to only authenticated users. In the resolve
method, we fetch the bit that the reply is for. Then we create a new reply containing the ID of the user leaving the reply and the reply content. Using the replies
relationship, we persist in the new reply to the database. Finally, we return the newly created reply.
Add the mutation to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
// ...
'mutation' => [
...,
'replyBit' => \App\GraphQL\Mutation\ReplyBitMutation::class,
]
]
],
Liking and unliking a bit
The last piece of functionality we’ll be adding is the ability for users to be able to like and unlike bits. For these, we’ll create two new mutations, which will be restricted to only authenticated users. First, the mutation to like a bit. Within app/GraphQL/Mutation
, create a new LikeBitMutation.php
file and paste the following code into it:
// app/GraphQL/Mutation/LikeBitMutation.php
<?php
namespace App\GraphQL\Mutation;
use App\Like;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
use App\Bit;
class LikeBitMutation extends Mutation
{
protected $attributes = [
'name' => 'likeBit'
];
public function type()
{
return Type::string();
}
public function args()
{
return [
'bit_id' => [
'name' => 'bit_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required'],
],
];
}
public function authenticated($root, $args, $currentUser)
{
return !!$currentUser;
}
public function resolve($root, $args)
{
$bit = Bit::find($args['bit_id']);
$like = new Like();
$like->user_id = auth()->user()->id;
$bit->likes()->save($like);
return 'Like successful!';
}
}
This a accepts the ID of the bit as its only argument. The resolve
method is similar to that of the ReplyBitMutation
, we fetch the bit then create and persist the like to the database. We then return a success message.
Next, let’s create the mutation to, unlike a bit. Within app/GraphQL/Mutation
, create a new UnlikeBitMutation.php
file and paste the following code into it:
// app/GraphQL/Mutation/UnlikeBitMutation.php
<?php
namespace App\GraphQL\Mutation;
use App\Like;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
class UnlikeBitMutation extends Mutation
{
protected $attributes = [
'name' => 'unlikeBit'
];
public function type()
{
return Type::string();
}
public function args()
{
return [
'bit_id' => [
'name' => 'bit_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['required'],
],
];
}
public function authenticated($root, $args, $currentUser)
{
return !!$currentUser;
}
public function resolve($root, $args)
{
$like = Like::where('user_id', auth()->user()->id)
->where('bit_id', $args['bit_id'])
->delete();
return 'Unlike successful!';
}
}
This mutation is pretty straightforward as it deletes alike where the user ID matches that of the authenticated user and where the bit ID matches that supplied in the argument. Finally, returns a success message.
Let’s add them to config/graphql.php
:
// config/graphql.php
'schemas' => [
'default' => [
// ...
'mutation' => [
...,
'likeBit' => \App\GraphQL\Mutation\LikeBitMutation::class,
'unlikeBit' => \App\GraphQL\Mutation\UnlikeBitMutation::class,
]
]
],
Testing the API
We need to make sure the server is up and running. Let’s start the server:
$ php artisan serve
We can test our API directly in the browser as below:
// fetch all bits
http://127.0.0.1:8000/graphql?query=query{allBits{id,user{name},snippet,replies{id,user{name},reply},likes_count,created_at,updated_at}}
// post a new bit
http://127.0.0.1:8000/graphql?query=mutation{newBit(snippet: "<?php ehco phpinfo(); ?>"){id,user{id,name},snippet,created_at,updated_at}}
But I prefer to test with Insomnia because it has support for GraphQL out of the box. The query to fetch all bits and their nested queries might look like below:
// fetch all bits
{
allBits {
id
user {
name
}
snippet
replies {
id
user {
name
}
reply
}
likes_count
created_at
updated_at
}
}
Similarly, the mutation to post a new bit:
// post a new bit
mutation {
newBit (snippet: "<?php ehco phpinfo(); ?>") {
id
user {
id
name
}
snippet
created_at
updated_at
}
}
We need to add the token that was returned upon log in to the Authorization header of the mutation request. From the Auth dropdown, select Bearer Token and paste the token (JWT) in the field provider. With that added, we can now post a new bit successfully.
Conclusion
In this tutorial, we have seen how to build an API using Laravel and GraphQL. We covered things like authentication, querying nested resources and eager loading related models. Feel free to play with this API by adding your own features, this will help solidify your understanding of working with Laravel and GraphQL.