Build API for Twitter like app using GraphQL in Laravel

Recently I have been working on something very amazing for API development, everyone has been using REST to access API, but now things are changing, in the front-end heavy world where things are consistently growing and new features added every week a fixed REST API is not very good option anymore, so what we need then? the answer is GraphQL. Let’s dive into details of this amazing query language and build a Twitter like app API in Laravel using GraphQL.

What is GraphQL?

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn’t tied to any specific database or storage engine and is instead backed by your existing code and data. It was created by Facebook to fulfill growing needs of API consumption and it has been implemented in major languages.

Types

At the heart of any GraphQL implementation is a description of what types of objects it can return, described in a GraphQL type system and returned in the GraphQL Schema. For example here is a User type which we will use later in this post.

type User {
  id: Integer
  name: String,
  email: String,
  password: String,
  avatar: String
}

Query

GraphQL queries declaratively describe what data the issuer wishes to fetch from whoever is fulfilling the GraphQL query. For example, if you want to get User with id and name you can query it like this:

query UserNameAndIDQuery {
  users {
    id,
    name
  }
}

which will give us only the name and id of users like this:

{  
   "data":{  
      "users":[  
         {  
            "id":1,
            "name":"Garnet Little"
         },
         {  
            "id":2,
            "name":"Lisa Moen MD"
         }
      ]
   }
}

Mutation

Most discussions of GraphQL focus on data fetching, but any complete data platform needs a way to modify server-side data as well, mutation can do that. These are set of queries which can perform create, update & delete on the database.

mutation {
    createUser( 
      name: "Saqueib Ansrai", 
      email: "hello@example.com", 
      password:"secret" 
    ) 
    { id, 
      email, 
      name 
    }
}

Above mutation will create a User and return back the id, email & name.

{
  "data" : {
    "createUser" : {
       "id": 1,
       "email": "hello@example.com",
       "name": "Saqueib Ansrai"
    }
  }
}

Now let’s see how we can use this in a Laravel App, I am going to use Laravel 5.4 and to implement server of GraphQL I am going to use laravel-graphql package. This PHP implementation is a thin wrapper around your existing data layer and business logic. It doesn’t dictate how these layers are implemented or which storage engines are used. Instead, it provides tools for creating API for your existing app.

Setup Laravel

Create a new Laravel 5.4 App and once installed configure a database and install laravel-graphql package using composer.

composer require folklore/graphql

Once that’s installed, add service provider and alias for the facade.

'providers' => [
    ...
    Folklore\GraphQL\ServiceProvider::class,
]

....
'aliases' => [
    ....
    'GraphQL' => Folklore\GraphQL\Support\Facades\GraphQL::class,
]

Now let’s publish our GraphQL config file

php artisan vendor:publish --provider="Folklore\GraphQL\ServiceProvider"

That completes installation and everything is setup. Let’s move on the Model and Migration.

Create models

We will be building a tweeter like an app DB schema, let’s add an avatar field on provided User model and create Profile, Tweet, Reply  & Like model with migration.

php artisan make:model Profile -m
php artisan make:model Tweet -m
php artisan make:model Reply -m
php artisan make:model Like -m

Now let’s fill up the migration schema in all stubs.

Schema::create('profiles', function (Blueprint $table) {
    $table->increments('id');
    $table->string('bio')->nullable();
    $table->string('designation')->nullable();
    $table->string('company')->nullable();
    $table->string('city', 70)->index()->nullable();
    $table->string('country', 100)->nullable();
    $table->date('dob')->nullable();

    $table->unsignedInteger('user_id');
    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');

    $table->timestamps();
});

Schema::create('tweets', function (Blueprint $table) {
    $table->increments('id');
    $table->string('body');

    $table->unsignedInteger('user_id');
    $table->foreign('user_id')
            ->references('id')
            ->on('users')
            ->onDelete('cascade');

    $table->timestamps();
});

Schema::create('replies', function (Blueprint $table) {
    $table->increments('id');
    $table->string('body');

    $table->unsignedInteger('user_id');
    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');

    $table->unsignedInteger('tweet_id');
    $table->foreign('tweet_id')
        ->references('id')
        ->on('tweets')
        ->onDelete('cascade');

    $table->timestamps();
});

Schema::create('likes', function (Blueprint $table) {
    $table->bigIncrements('id');

    $table->unsignedInteger('user_id');
    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');

    $table->unsignedInteger('tweet_id');
    $table->foreign('tweet_id')
        ->references('id')
        ->on('tweets')
        ->onDelete('cascade');

    $table->unique(['user_id', 'tweet_id']);
    $table->timestamps();
});

I have gone ahead and created Model factory and Database seeder for this. Run the php artisan db:seed to seed dummy data.

Now laravel stuff out of the way lets now start defining the Types for User to use with GraphQL.

User Type

Let’s define the UserType, we need to create our class in app/GraphQL/Type/UserType.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'
    ];

    /*
     * Uncomment following line to make the type input object.
     * http://graphql.org/learn/schema/#input-types
     */
    // protected $inputObject = true;

    public function fields()
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::int()),
                'description' => 'The id of the user'
            ],
            'name' => [
                'type' => Type::string(),
                'description' => 'The name of user'
            ],
            'email' => [
                'type' => Type::string(),
                'description' => 'The email of user'
            ],
            'avatar' => [
                'type' => Type::string(),
                'description' => 'The avatar of user'
            ],
            'created_at' => [
                'type' => Type::string(),
                'description' => 'Creation datetime'
            ],
            'updated_at' => [
                'type' => Type::string(),
                'description' => 'Updating datetime'
            ],

            // Nested Resource
            'tweets' => [
                'args' => [
                    'id' => [
                        'type' => Type::int(),
                        'description' => 'id of the tweet',
                    ],
                    'first' => [
                        'type' => Type::int(),
                        'description' => 'limit result',
                    ],
                ],
                'type' => Type::listOf(GraphQL::type('Tweet')),
                'description' => 'tweet description',
            ],
            'profile' => [
                'type' => GraphQL::type('Profile'),
                'description' => 'user profile',
            ]
        ];
    }

    // If you want to resolve the field yourself, you can declare a method
    // with the following format resolve[FIELD_NAME]Field()
    protected function resolveEmailField($root, $args)
    {
        return strtolower($root->email);
    }

    protected function resolveCreatedAtField($root, $args)
    {
        return (string) $root->created_at;
    }

    // You can also resolve any nested resource filed in same way
    protected function resolveTweetsField($root, $args)
    {
        $tweets = $root->tweets();

        if (isset($args['id'])) {
            return  $tweets->where('id', $args['id']);
        }

        // check for limit
        if( isset($args['first']) ) {
            $tweets =  $tweets->limit($args['first'])->latest();
        }

        return $tweets->get();
    }

    protected function resolveProfileField($root, $args) {
        return $root->profile;
    }
}

Let me explain whats going on, GraphQL need 2 things to return data for a field, first is a fields and second is a resolver function which will get the data from the database for that field. In above class fields() method we are returning a fields schema for user, only fields added here will be returned by GraphQL API.

And below fields() we have resolver overrides, for example I have used resolveEmailField() method to format the email to make it lowercase. It’s like Accessor in Eloquent model, and after this we have some nested resources tweets and profile which are resolved by resolveTweetsField() method and resolveProfileField() respectively.

In Tweets resolver, we have some argument checking, which will be passed from the query and we return the has many relation tweets data from $root which is User model instance here.

Once you defined all the type you need to add them in config/graphql.php files under types array, thats how GraphQL will know about available types.

'types' => [
    'User' => App\GraphQL\Type\UserType::class,
    'Profile' => App\GraphQL\Type\ProfileType::class,
    'Tweet' => App\GraphQL\Type\TweetType::class,
    'Reply' => App\GraphQL\Type\ReplyType::class,
    'Like' => App\GraphQL\Type\LikeType::class
],

Cool, now we have our Types, let’s define our Queries so we can access data through API call using GraphQL.

User Query

We want to create a query which will be for only reading data. Let’s define one to access Users, create a class under below namespace.

namespace App\GraphQL\Query;

use GraphQL;
use App\User;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Query;

class UsersQuery extends Query {

    protected $attributes = [
        'name' => 'users'
    ];

    public function type()
    {
        return Type::listOf(GraphQL::type('User'));
    }

    public function args()
    {
        return [
            'id' => ['name' => 'id', 'type' => Type::int()],
            'email' => ['name' => 'email', 'type' => Type::string()],
            'first' => ['name' => 'first', 'type' => Type::int()],
        ];
    }

    public function resolve($root, $args)
    {
        $user = new User;

        // check for limit
        if( isset($args['first']) ) {
            $user =  $user->limit($args['first'])->latest('id');
        }

        if(isset($args['id']))
        {
            $user = $user->where('id' , $args['id']);
        }

        if(isset($args['email']))
        {
            $user = $user->where('email', $args['email']);
        }

        return $user->get();
    }
}

For sake of simplicity I have added all the resolver data fetching logic in the method, you should consider a service class to delegate fetching of data so later you can add caching etc.

This looks very similar to UserType class but it will be use to fetch the actual data, so first, we define the Type we are going to return in type() method, in this case it will be Type::listOf(GraphQL::type('User')). Next we define all of the argument this query can take in args() method. In here we have id which we can use to filter user by id, email for same and first which will be used to limit the result by number.

Once you defined your Query, let’s once again add it in config/graphql.php under schemas default queries array.

'schemas' => [
    'default' => [
        'query' => [
            'users' => App\GraphQL\Query\UsersQuery::class
           ...
        ],
    ]
]

GraphQL users query

Now we can open a browser and get the user data. Let’s get all the users with id and name, by default you can access on http://localhost:8000/graphql route, you can change this in graphql.php config file.

I am running on localhost:8000, you need to match your dev app url to test it.

Here is how our query looks like:

query getUserNameAndId {
    users{
     id,
     name
    }
}

and a request URL will be like this

http://localhost:8000/graphql?query=query+getUserNameAndId{users{id,name}}

graphql-get-user-query

If you did everything right you will get the list of users, if something is wrong you will get an error with a message.

Limit Result

If you remember in query definition, we had some arguments, let’s use them. First let’s limit the result to only 5 users, passing first:5 will do the trick.

query getUserNameAndId {
    users(first: 5){
     id,
     name
    }
}

Request URL

http://localhost:8000/graphql?query=query+getUserNameAndId{users(first:5){id,name}}

graphql-query-user-limit

Find user by ID or Email

You can also find a specific user by id or email since we have defined arguments in our UsersQuery.

query getUserById {
    users(id:2){
     id,
     name
    }
}

query getUserByEmail {
 users(email:"saqueib@example.com"){
 id,
 name
 }
}

Requests URL

http://localhost:8000/graphql?query=query+getUserById{users(id:5){id,name}}
http://localhost:8000/graphql?query=query+getUserByEmailId{users(email:"saqueib@example.com"){id,name}}

graphql-get-user-by-id-and-email

Access Nested Resource

Accessing the nested resource where GraphQL shines, in UserType we have defined tweets and profile as nested resource. Let see how we can get them in a query.

query getUserWithProfile {
    users(first: 3){
     id,
     name,
     profile {
      bio,
      city,
      country
     }
    }
}

Request URL

http://localhost:8000/graphql?query=query+getUserWithProfile{users(first:3){id,name,profile{bio,city,country}}}

Similarly, you can access tweets of a user, let’s access first 3 tweets for a user.

query getUserWithTweets {
    users(first: 3){
     id,
     name,
     tweets(first: 3) {
      body,
      created_at
     }
    }
}

Request URL

http://localhost:8000/graphql?query=query+getUserWithTweets{users(first:3){id,name,tweets(first:3){body,created_at}}}

graphql-user-with-tweets

Power of GraphQL queries

As you can see you have lots of flexibility in terms of what exactly you need, if you do the same thing in REST API you probably need 3 different HTTP request to get all the things you needed or create some sort of alias endpoint on API to get userWithTweet and another for userWithProfile. It will make more difficult to change front-end of app since data need has changed, now you probably need another API endpoint with userProfileAndTweets. As you can see it can get very messy very quickly. But with GraphQL you don’t need any other endpoint, it will be most likely will be a single endpoint and queries declaratively describe what data the issuer wishes to fetch.

Now we have a solid understanding how to fetch data, but what about making a change in data or persist new record.

Mutation in GraphQL

Let’s define the mutation to createUserupdateUser and deleteUser.

Let’s create a file in app/GraphQL/Mutation/CreateUserMutation.php and add below code.

namespace App\GraphQL\Mutation;

use GraphQL;
use App\User;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;

class CreateUserMutation extends Mutation {

    protected $attributes = [
        'name' => 'createUser'
    ];

    public function type()
    {
        return GraphQL::type('User');
    }

    public function rules()
    {
        return [
            'email' => 'required|email|unique:users',
            'name' => 'required|min:2',
            'password' => 'required|min:6',
            'avatar' => 'url'
        ];
    }

    public function args()
    {
        return [
            'name' => ['name' => 'name', 'type' => Type::string()],
            'email' => ['name' => 'email', 'type' => Type::string()],
            'password' => ['name' => 'password', 'type' => Type::string()],
            'avatar' => ['name' => 'avatar', 'type' => Type::string()],
        ];
    }

    public function resolve($root, $args)
    {
        return User::create($args);
    }
}

Mutation needs to mutate the data so it has one more rules() method to validate the data, you can use laravel validation with a resolve function that does the data manipulation in the database. In this case, we create a user if data is valid. Let’s see it in action.

As we did for queries, let’s add the mutation in config/graphql.php file under schemas default mutations array.

'schemas' => [
    'default' => [
        'query' => [
            'users' => App\GraphQL\Query\UsersQuery::class
            ...
        ],
        'mutation' => [
            'createUser' => App\GraphQL\Mutation\CreateUserMutation::class,
            'updateUser' => App\GraphQL\Mutation\UpdateUserMutation::class,
            'deleteUser' => App\GraphQL\Mutation\DeleteUserMutation::class
        ]
    ]
]

UserCreate Mutation

To create a user we need to send the user data, its looks like query but this time we have to pass key value pairs, unlike in query you just ask for fields.

mutation {
    createUser(
        name:"Saqueib Ansari", 
        email:"hello@example.com", 
        password:"secret"
    )
    {
        id,
        email,
        name
    }
}

In this createUser mutation we create the user and in next block, we are asking to return the created users id, email & name.

Request URL

http://localhost:8000/graphql?query=mutation+users{createUser(name:"Saqueib Ansrai",email:"hello@example.com",password:"secret"){id,email,name}}

graphql-mutation-create-user

UserUpdate Mutation

Now let’s update a User using,UpdateUserMutation here is the resolve function of this.

public function resolve($root, $args)
    {
        $user = User::find($args['id']);

        if(! $user)
        {
            return null;
        }

        // update user
        $fields = isset($args['password'])
                ? array_merge($args, ['password' => bcrypt($args['password'])])
                : $args;

        $user->update($fields);

        return $user;
    }

As you can see we asked id argument from mutation query and trying to find a user with that, later we update it.

mutation updateUsersEmail{
    updateUser(
        id:2,
        email:"email@example.com"
    )
    {
        id,
        email,
        name
    }
}

In this mutation, we are updating user email with id 2.

Request URL

http://localhost:8000/graphql?query=mutation+updateUsersEmail{updateUser(id:2,email:"email@example.com"){id,email,name}}

graphql-mutation-update-user

UserDelete Mutation

In delete mutation, if you just pass id and you can delete a user.

public function resolve($root, $args)
{
    if( $user = User::findOrFail($args['id']) ) {
        $user->delete();
        return $user;
    }

    return null;
}

And now query will be like this:

mutation deleteUserById{
    deleteUser(
        id:2
    )
    {
        id,
        email,
        name
    }
}

This call will delete the user with id 2 and return the deleted user id, name & email.

Request URL

http://localhost:8000/graphql?query=mutation+deleteUserById{deleteUser(id:2){id,email,name}}

With that we have a complete CRUD operation in GraphQL, I will create Follow and Tweet queries and mutation and it will be available in GitHub repo.

Conclusion

Although GraphQL is very cool and defiantly going to replace REST in future but at the time writing of this post GraphQL is still a specification and not very well implemented in laravel community, the package we have used laravel-graphql is still a work in progress as of 15th Aug 2017, but I think the amount of flexibility GraphQL is providing its worth the time to learn it. I have not covered all the code for brevity but you can access it at GitHub. I would love to hear from you. In next part, I will be building a twitter like app in Vue.js with all the bells and whistles possible using this GraphQL API soon. As always if you like it please comment I love to hear your thoughts and feedback on it.

Source Code

  • Shane Rosenthal

    Wow, this is a game changer. Thanks!

  • What I’d really like to see is a post on high performance graphql in Laravel. Ya know with eager loading of relations based on the ResolveInfo data. Ideally covering each relation type off both a root and nested node.

    Realistically this is the only thing stopping us from taking up graphql given that we often build a page and find it’s issuing 600+ queries – implementing eager loading usually takes it down to about 7 and can cut seconds off the load time

    • Saqueib

      Totally agree with this, in next part i will try to optimize queries and build a SPA for frontent in vue.js

      • Omsun Kumar

        thanks , now i am able to work it out

  • Omsun Kumar

    i get method not allowed exception error
    i am using this link http://localhost/kripro/jsemantic/graphql?query=query+getUserNameAndId{users{id,name}}

    • Saqueib

      please run php artisan route:list to check if graphql route is present and pointing to right controller method

      | | GET|HEAD | graphiql/{graphql_schema?} | graphql.graphiql | FolkloreGraphQLGraphQLController@graphiql | |
      | | GET|HEAD | graphql/{graphql_schema?} | graphql.query | FolkloreGraphQLGraphQLController@query | graphql,auth |
      | | POST | graphql/{graphql_schema?} | graphql.query.post | FolkloreGraphQLGraphQLController@query | graphql,auth |

      • Omsun Kumar

        no its not there what should i do to get that in the list

        • Saqueib

          Please add below in config/app.php


          'providers' => [
          ...
          FolkloreGraphQLServiceProvider::class,
          ]

          and the publish the config by running php artisan vendor:publish --provider="FolkloreGraphQLServiceProvider" you will see config/graphql.php

          ‘controllers’ => FolkloreGraphQLGraphQLController::class.’@query’,

          which register above routes.

  • Omsun Kumar
  • Omsun Kumar

    thanks a lot ,it helped me , just a small query how i can have this.
    {
    “status”:200,
    “message”:”Success”,
    “data”: {
    “deleteUser”: {
    “id”: 15
    }
    }
    }