Building APIs with Laravel and GraphQL

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 appdirectory. Within the GraphQL directory, create a new Type directory. Within app/GraphQL/Typedirectory, 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.phpfile 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 (userreplies, 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 Querydirectory, 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.

You May Also Like
Read More

Froxt DevDay

It’s almost time! Froxt DevDay – The Meetup, a fresh take on our yearly virtual developer event, begins…
Read More

Introducing Froxt DCP

In 2021, we started an initiative called Froxt Work Management (FWM) Cloud First Releases for Ecosystem which enabled…