Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

How to Build a GraphQL API with Laravel

Published

LaravelGraphQL

GraphQL is a query language and runtime for APIs that was developed by Facebook in 2012 and open-sourced in 2015. Unlike traditional REST APIs where you have multiple endpoints for different resources, GraphQL provides a single endpoint that can handle all your data requests.

Key Benefits of GraphQL:

Precise Data Fetching:

Instead of getting all user data, you can request only the name and email:

{
  user(id: 1) {
    name
    email
  }
}

Single Request for Related Data:

Get a user with their posts in one request:

{
  user(id: 1) {
    name
    posts {
      title
      content
    }
  }
}

Strong Type System: Your API schema defines exactly what data is available and what types it should be.

Self-Documenting: GraphQL APIs come with built-in documentation and introspection.

GraphQL vs REST

REST GraphQL
Multiple endpoints (/users, /posts) Single endpoint (/graphql)
Fixed data structure Flexible data fetching
Over-fetching/under-fetching issues Get exactly what you need
Multiple requests for related data Single request for complex data

Why Laravel + GraphQL?

Laravel’s robust ecosystem combined with GraphQL’s flexibility creates a powerful API development experience. Laravel provides excellent database handling, authentication, and validation, while GraphQL offers precise data fetching and excellent developer tooling.

Let’s build a practical blog API to see how these technologies work together.

What You’ll Build

We’ll create a simple blog API with two main resources:

  • Users – who can write articles
  • Articles – blog posts with authors

Setting Up Laravel Project

First, create a new Laravel project:

composer create-project laravel/laravel graphql-blog
cd graphql-blog

Configure your database in .env:

DB_CONNECTION=sqlite
DB_DATABASE=database/database.sqlite

Create the SQLite database:

touch database/database.sqlite

Database Models and Migrations

Update User Migration

Edit the existing user migration to add an API token:

// database/migrations/create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->string('api_token', 80)->unique()->nullable();
    $table->rememberToken();
    $table->timestamps();
});

Create Article Model and Migration

php artisan make:model Article -m

Update the Article migration:

// database/migrations/create_articles_table.php
Schema::create('articles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('title');
    $table->text('content');
    $table->timestamps();
});

Define Model Relationships

Update your models:

// app/Models/User.php
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;


    protected $fillable = ['name', 'email', 'password'];
    protected $hidden = ['password', 'remember_token'];


    public function articles()
    {
        return $this->hasMany(Article::class);
    }
}
// app/Models/Article.php
class Article extends Model
{
    use HasFactory;


    protected $fillable = ['title', 'content', 'user_id'];


    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Run Migrations and Seed Data

php artisan migrate

Create some test data using Tinker:

php artisan tinker
// In Tinker
$user = User::factory()->create([
    'name' => 'John Doe',
    'email' => '[email protected]',
    'password' => bcrypt('password')
]);


Article::factory()->create([
    'title' => 'My First Article',
    'content' => 'This is the content of my first article.',
    'user_id' => $user->id
]);

Installing Lighthouse GraphQL

Install Lighthouse package:

composer require nuwave/lighthouse

Publish the configuration:

php artisan vendor:publish --tag=lighthouse-config

Create the GraphQL schema directory:

mkdir graphql

Creating GraphQL Schema

Create your GraphQL schema file:

# graphql/schema.graphql


"A datetime string with format `Y-m-d H:i:s`"
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")


type Query {
    # User queries
    users: [User!]! @paginate
    user(id: ID! @eq): User @find
    me: User @auth(guard: "api")
    
    # Article queries
    articles: [Article!]! @paginate
    article(id: ID! @eq): Article @find
}


type Mutation {
    # Authentication
    login(email: String!, password: String!): AuthPayload
        @field(resolver: "AuthResolver@login")
    
    # User operations
    createUser(
        name: String!
        email: String! @rules(apply: ["email", "unique:users"])
        password: String! @rules(apply: ["min:6"]) @bcrypt
    ): User @create
    
    # Article operations
    createArticle(
        title: String!
        content: String!
    ): Article @field(resolver: "ArticleResolver@create") @guard(with: ["api"])
    
    updateArticle(
        id: ID!
        title: String
        content: String
    ): Article @field(resolver: "ArticleResolver@update") @guard(with: ["api"])
    
    deleteArticle(id: ID!): Article @delete @guard(with: ["api"])
}


type User {
    id: ID!
    name: String!
    email: String!
    articles: [Article!]! @hasMany
    created_at: DateTime!
    updated_at: DateTime!
}


type Article {
    id: ID!
    title: String!
    content: String!
    user: User! @belongsTo
    created_at: DateTime!
    updated_at: DateTime!
}


type AuthPayload {
    token: String!
    user: User!
}

Implementing Queries

The basic queries are handled automatically by Lighthouse directives:

  • @all – fetches all records
  • @paginate – adds pagination
  • @find – finds a single record
  • @eq – adds where clause

Test your queries by starting the server:

php artisan serve

Visit http://localhost:8000/graphql-playground and try:

query {
  users {
    data {
      id
      name
      email
      articles {
        id
        title
      }
    }
  }
}

Building Mutations

Create Authentication Resolver

php artisan make:class GraphQL/Resolvers/AuthResolver
<?php
// app/GraphQL/Resolvers/AuthResolver.php


namespace App\GraphQL\Resolvers;


use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use GraphQL\Error\Error;


class AuthResolver
{
    public function login($root, array $args)
    {
        $user = User::where('email', $args['email'])->first();


        if (!$user || !Hash::check($args['password'], $user->password)) {
            throw new Error('Invalid credentials');
        }


        $token = Str::random(60);
        $user->api_token = $token;
        $user->save();


        return [
            'token' => $token,
            'user' => $user
        ];
    }
}

Create Article Resolver

php artisan make:class GraphQL/Resolvers/ArticleResolver
<?php
// app/GraphQL/Resolvers/ArticleResolver.php


namespace App\GraphQL\Resolvers;


use App\Models\Article;
use Illuminate\Support\Facades\Auth;
use GraphQL\Error\Error;


class ArticleResolver
{
    public function create($root, array $args)
    {
        $user = Auth::user();
        
        $article = new Article($args);
        $article->user_id = $user->id;
        $article->save();


        return $article;
    }


    public function update($root, array $args)
    {
        $article = Article::findOrFail($args['id']);
        
        // Check if user owns the article
        if ($article->user_id !== Auth::id()) {
            throw new Error('Unauthorized');
        }


        $article->update(array_filter($args, fn($value) => $value !== null));
        
        return $article;
    }
}

Adding Authentication

Update your API configuration in config/auth.php:

'guards' => [
    'api' => [
        'driver' => 'token',
        'provider' => 'users',
        'hash' => false,
    ],
],

Testing Your API

1. Create a User

mutation {
  createUser(
    name: "Jane Doe"
    email: "[email protected]"
    password: "password123"
  ) {
    id
    name
    email
  }
}

2. Login

mutation {
  login(email: "[email protected]", password: "password123") {
    token
    user {
      id
      name
    }
  }
}

3. Create Article (with Authorization)

Add the token to headers in GraphQL Playground:

{
  "Authorization": "Bearer YOUR_TOKEN_HERE"
}

Then create an article:

mutation {
  createArticle(
    title: "My GraphQL Journey"
    content: "Learning GraphQL with Laravel has been amazing!"
  ) {
    id
    title
    user {
      name
    }
  }
}

4. Query Articles with Relationships

query {
  articles(first: 5) {
    data {
      id
      title
      content
      user {
        name
        email
      }
    }
    paginatorInfo {
      total
      hasMorePages
    }
  }
}

5. Get Current User

query {
  me {
    id
    name
    email
    articles {
      id
      title
      created_at
    }
  }
}

Production Tips

Performance Optimization

1. Query Complexity Analysis

Prevent expensive queries by limiting complexity:

// config/lighthouse.php
'security' => [
    'max_query_complexity' => 1000,
    'max_query_depth' => 15,
    'disable_introspection' => env('LIGHTHOUSE_DISABLE_INTROSPECTION', false),
]

2. Caching

Cache frequently requested data:

// In your resolver
use Illuminate\Support\Facades\Cache;


public function popularArticles()
{
    return Cache::remember('popular_articles', 3600, function () {
        return Article::with('user')->orderBy('views', 'desc')->take(10)->get();
    });
}

3. DataLoader Pattern

Lighthouse automatically handles N+1 query problems, but you can optimize further:

type Query {
    articles: [Article!]! @paginate
        @with(relation: "user")  # Eager load users
}

Security Best Practices

1. Disable Introspection in Production

LIGHTHOUSE_DISABLE_INTROSPECTION=true

2. Rate Limiting

Add rate limiting to your GraphQL endpoint:

// routes/web.php
Route::middleware(['throttle:60,1'])->group(function () {
    Route::post('/graphql', [\Nuwave\Lighthouse\Support\Http\Controllers\GraphQLController::class, 'query']);
});

3. Input Validation

Always validate inputs in your schema:

createUser(
    email: String! @rules(apply: ["email", "unique:users"])
    password: String! @rules(apply: ["min:8", "confirmed"])
): User

Monitoring and Debugging

1. Query Logging

Enable query logging for debugging:

// AppServiceProvider.php
use Illuminate\Support\Facades\DB;


public function boot()
{
    if (app()->environment('local')) {
        DB::listen(function ($query) {
            Log::info('Query: ' . $query->sql . ' [' . implode(', ', $query->bindings) . ']');
        });
    }
}

2. GraphQL Playground

Use GraphQL Playground for development and testing:

# Available at: http://localhost:8000/graphql-playground

Conclusion

Building a GraphQL API with Laravel offers significant advantages over traditional REST APIs. You get precise data fetching, strong typing, excellent developer tools, and Laravel’s robust ecosystem all in one package.

Key Benefits You’ve Achieved:

  • Efficient Data Fetching: Clients request only the data they need
  • Single Endpoint: No more managing multiple REST endpoints
  • Type Safety: GraphQL schema ensures consistent data structure
  • Great Developer Experience: Built-in documentation and query playground
  • Laravel Integration: Seamless use of Eloquent models and relationships

What You’ve Built:

  • Complete CRUD operations for articles
  • User authentication with API tokens
  • Database relationships between users and articles
  • Input validation and authorization
  • Pagination for large datasets
  • Production-ready security features

This foundation can easily be extended with features like real-time subscriptions, file uploads, advanced search, and more complex business logic.

GraphQL with Laravel provides a modern, efficient approach to API development that scales well from small projects to enterprise applications.

Related guides

Account services are unavailable until the production backend is configured.