GraphQL is revolutionizing API development. Instead of multiple REST endpoints returning fixed data structures, GraphQL lets clients request exactly what they need in a single query. At ZIRA Software, we've built GraphQL APIs serving complex data requirements with unprecedented flexibility and performance.
Why GraphQL over REST?
GraphQL advantages:
- Single endpoint for all queries
- Clients specify exact data needs
- No over-fetching or under-fetching
- Strong typing system
- Self-documenting APIs
- Nested queries in single request
- Real-time subscriptions
REST problems GraphQL solves:
REST: Multiple endpoints, fixed responses
GET /users/1 → { id, name, email, ... }
GET /users/1/posts → [{ id, title, ... }, ...]
GET /posts/1/comments → [{ id, body, ... }, ...]
GraphQL: One endpoint, flexible queries
POST /graphql
{
user(id: 1) {
name
email
posts {
title
comments { body }
}
}
}
Setup GraphQL in Laravel
Install Lighthouse
composer require nuwave/lighthouse
Lighthouse is the best GraphQL library for Laravel, providing seamless integration with Eloquent.
Publish Configuration
php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider"
config/lighthouse.php:
<?php
return [
'schema' => [
'register' => base_path('graphql/schema.graphql'),
],
'namespaces' => [
'models' => 'App',
'queries' => 'App\\GraphQL\\Queries',
'mutations' => 'App\\GraphQL\\Mutations',
],
'route' => [
'uri' => '/graphql',
'middleware' => ['api'],
],
];
Schema Definition
graphql/schema.graphql:
type Query {
users: [User!]! @all
user(id: ID! @eq): User @find
posts: [Post!]! @all
post(id: ID! @eq): Post @find
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! @hasMany
created_at: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User! @belongsTo
comments: [Comment!]! @hasMany
published_at: DateTime
created_at: DateTime!
}
type Comment {
id: ID!
body: String!
author: User! @belongsTo
post: Post! @belongsTo
created_at: DateTime!
}
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
Queries
Simple Queries
Get all users:
query {
users {
id
name
email
}
}
Response:
{
"data": {
"users": [
{ "id": "1", "name": "John Doe", "email": "john@example.com" },
{ "id": "2", "name": "Jane Smith", "email": "jane@example.com" }
]
}
}
Get specific user:
query {
user(id: 1) {
id
name
email
}
}
Nested Queries
Get user with posts:
query {
user(id: 1) {
name
email
posts {
id
title
published_at
}
}
}
Get user with posts and comments:
query {
user(id: 1) {
name
posts {
title
comments {
body
author {
name
}
}
}
}
}
Response:
{
"data": {
"user": {
"name": "John Doe",
"posts": [
{
"title": "GraphQL Introduction",
"comments": [
{
"body": "Great post!",
"author": { "name": "Jane Smith" }
}
]
}
]
}
}
}
Custom Queries
app/GraphQL/Queries/SearchPosts.php:
<?php namespace App\GraphQL\Queries;
use App\Post;
class SearchPosts
{
public function __invoke($root, array $args)
{
$query = Post::query();
if (isset($args['title'])) {
$query->where('title', 'like', '%' . $args['title'] . '%');
}
if (isset($args['author'])) {
$query->whereHas('author', function ($q) use ($args) {
$q->where('name', 'like', '%' . $args['author'] . '%');
});
}
if (isset($args['published'])) {
if ($args['published']) {
$query->whereNotNull('published_at');
} else {
$query->whereNull('published_at');
}
}
return $query->get();
}
}
Add to schema:
type Query {
searchPosts(
title: String
author: String
published: Boolean
): [Post!]! @field(resolver: "App\\GraphQL\\Queries\\SearchPosts")
}
Use query:
query {
searchPosts(title: "GraphQL", published: true) {
id
title
author {
name
}
}
}
Mutations
Add to Schema
type Mutation {
createPost(input: CreatePostInput!): Post @create
updatePost(id: ID!, input: UpdatePostInput!): Post @update
deletePost(id: ID!): Post @delete
}
input CreatePostInput {
title: String! @rules(apply: ["required", "max:255"])
content: String! @rules(apply: ["required"])
author_id: ID! @rules(apply: ["required", "exists:users,id"])
}
input UpdatePostInput {
title: String @rules(apply: ["max:255"])
content: String
published_at: DateTime
}
Using Mutations
Create post:
mutation {
createPost(input: {
title: "Getting Started with GraphQL"
content: "GraphQL is a query language..."
author_id: 1
}) {
id
title
author {
name
}
}
}
Update post:
mutation {
updatePost(
id: 1
input: {
title: "Updated Title"
published_at: "2016-01-15 10:00:00"
}
) {
id
title
published_at
}
}
Delete post:
mutation {
deletePost(id: 1) {
id
title
}
}
Custom Mutations
app/GraphQL/Mutations/PublishPost.php:
<?php namespace App\GraphQL\Mutations;
use App\Post;
use Carbon\Carbon;
class PublishPost
{
public function __invoke($root, array $args)
{
$post = Post::findOrFail($args['id']);
$post->update([
'published_at' => Carbon::now()
]);
// Fire event
event(new PostPublished($post));
return $post;
}
}
Schema:
type Mutation {
publishPost(id: ID!): Post @field(resolver: "App\\GraphQL\\Mutations\\PublishPost")
}
Pagination
Schema:
type Query {
posts(first: Int!, page: Int): PostPaginator
}
type PostPaginator {
data: [Post!]!
paginatorInfo: PaginatorInfo!
}
type PaginatorInfo {
currentPage: Int!
lastPage: Int!
perPage: Int!
total: Int!
}
Lighthouse directive:
type Query {
posts(first: Int!, page: Int): [Post!]! @paginate
}
Query:
query {
posts(first: 10, page: 1) {
data {
id
title
}
paginatorInfo {
currentPage
lastPage
total
}
}
}
Authentication
JWT Authentication
Middleware:
<?php namespace App\Http\Middleware;
use Closure;
use Tymon\JWTAuth\Facades\JWTAuth;
class AuthenticateGraphQL
{
public function handle($request, Closure $next)
{
try {
$user = JWTAuth::parseToken()->authenticate();
} catch (\Exception $e) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $next($request);
}
}
Protected queries:
type Query {
me: User @auth
myPosts: [Post!]! @field(resolver: "App\\GraphQL\\Queries\\MyPosts") @auth
}
app/GraphQL/Queries/MyPosts.php:
<?php namespace App\GraphQL\Queries;
use Illuminate\Support\Facades\Auth;
class MyPosts
{
public function __invoke()
{
return Auth::user()->posts;
}
}
Login mutation:
type Mutation {
login(email: String!, password: String!): AuthPayload
}
type AuthPayload {
access_token: String!
token_type: String!
expires_in: Int!
user: User!
}
app/GraphQL/Mutations/Login.php:
<?php namespace App\GraphQL\Mutations;
use Tymon\JWTAuth\Facades\JWTAuth;
class Login
{
public function __invoke($root, array $args)
{
$credentials = [
'email' => $args['email'],
'password' => $args['password']
];
if (!$token = JWTAuth::attempt($credentials)) {
throw new \Exception('Invalid credentials');
}
return [
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60,
'user' => auth()->user()
];
}
}
Field-Level Authorization
Schema:
type Post {
id: ID!
title: String!
content: String!
author: User!
earnings: Float @can(ability: "viewEarnings", model: "Post")
}
Policy:
<?php namespace App\Policies;
use App\User;
use App\Post;
class PostPolicy
{
public function viewEarnings(User $user, Post $post)
{
return $user->id === $post->author_id;
}
}
Filtering and Sorting
Schema:
type Query {
posts(
orderBy: [OrderByClause!]
where: PostWhereConditions
): [Post!]! @all
}
input OrderByClause {
field: String!
order: SortOrder!
}
enum SortOrder {
ASC
DESC
}
input PostWhereConditions {
title: String
author_id: ID
published: Boolean
}
Using Lighthouse:
type Query {
posts(
orderBy: [PostOrderByClause!]
@orderBy
): [Post!]! @all
filterPosts(
title: String @where(operator: "like")
author_id: ID @eq
published: Boolean @scope(name: "published")
): [Post!]! @all
}
input PostOrderByClause {
field: PostOrderByField!
order: SortOrder!
}
enum PostOrderByField {
ID
TITLE
CREATED_AT
PUBLISHED_AT
}
Query:
query {
posts(
orderBy: [{ field: PUBLISHED_AT, order: DESC }]
where: { published: true }
) {
title
published_at
}
}
N+1 Query Problem
Without optimization:
query {
posts {
title
author { # N+1 query!
name
}
}
}
Solution with @with directive:
type Query {
posts: [Post!]! @all
}
type Post {
author: User! @belongsTo(relation: "author")
}
Lighthouse automatically eager loads relationships!
Manual optimization:
<?php namespace App\GraphQL\Queries;
use App\Post;
class Posts
{
public function __invoke()
{
return Post::with('author', 'comments.author')->get();
}
}
DataLoader Pattern
app/GraphQL/Queries/BatchUsers.php:
<?php namespace App\GraphQL\Queries;
use App\User;
class BatchUsers
{
protected $cache = [];
public function __invoke($root, array $args)
{
$ids = $args['ids'];
$uncachedIds = array_diff($ids, array_keys($this->cache));
if (!empty($uncachedIds)) {
$users = User::whereIn('id', $uncachedIds)->get();
foreach ($users as $user) {
$this->cache[$user->id] = $user;
}
}
return collect($ids)->map(function ($id) {
return $this->cache[$id] ?? null;
})->filter()->values();
}
}
Subscriptions (Real-time)
Schema:
type Subscription {
postPublished: Post
}
Broadcasting:
<?php namespace App\Events;
use App\Post;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class PostPublished implements ShouldBroadcast
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
public function broadcastOn()
{
return ['graphql'];
}
}
Client subscription:
subscription {
postPublished {
id
title
author {
name
}
}
}
Error Handling
Custom exceptions:
<?php namespace App\Exceptions;
use GraphQL\Error\ClientAware;
class CustomException extends \Exception implements ClientAware
{
public function isClientSafe()
{
return true;
}
public function getCategory()
{
return 'custom';
}
}
Response:
{
"errors": [
{
"message": "Resource not found",
"extensions": {
"category": "custom"
},
"path": ["user"]
}
],
"data": {
"user": null
}
}
Testing
tests/GraphQL/PostTest.php:
<?php
use Illuminate\Foundation\Testing\DatabaseMigrations;
class PostTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function it_queries_posts()
{
factory(App\Post::class, 3)->create();
$query = '
query {
posts {
id
title
}
}
';
$response = $this->graphQL($query);
$response->assertJsonCount(3, 'data.posts');
}
/** @test */
public function it_creates_post()
{
$user = factory(App\User::class)->create();
$mutation = '
mutation {
createPost(input: {
title: "Test Post"
content: "Test content"
author_id: ' . $user->id . '
}) {
id
title
}
}
';
$response = $this->graphQL($mutation);
$response->assertJson([
'data' => [
'createPost' => [
'title' => 'Test Post'
]
]
]);
$this->assertDatabaseHas('posts', ['title' => 'Test Post']);
}
protected function graphQL($query, $variables = [])
{
return $this->postJson('/graphql', [
'query' => $query,
'variables' => $variables
]);
}
}
GraphQL Playground
Access GraphQL Playground at /graphql-playground for interactive API exploration:
- Auto-complete queries
- Schema documentation
- Query history
- Variable editor
Enable in config:
'route' => [
'playground' => env('GRAPHQL_PLAYGROUND_ENABLED', true),
],
Performance Optimization
1. Query complexity analysis:
'security' => [
'max_query_complexity' => 1000,
'max_query_depth' => 10,
],
2. Persisted queries:
// Client sends query hash instead of full query
{
"query_id": "abc123"
}
3. Response caching:
<?php namespace App\GraphQL\Queries;
use Illuminate\Support\Facades\Cache;
class Posts
{
public function __invoke()
{
return Cache::remember('graphql.posts', 60, function () {
return Post::with('author')->get();
});
}
}
4. Batch database queries:
type Post {
author: User! @belongsTo # Lighthouse batches these automatically
}
Best Practices
- Design schema first - Think about client needs
- Use pagination - Don't return unlimited results
- Authorize fields - Not just queries/mutations
- Handle N+1 queries - Use eager loading
- Version with fields - Add new fields, deprecate old
- Document schema - Use descriptions
- Validate input - Use input types and rules
- Monitor performance - Track query complexity
Conclusion
GraphQL with Laravel provides unprecedented flexibility in API development. Clients get exactly the data they need, developers maintain a single endpoint, and Lighthouse makes Laravel integration seamless. At ZIRA Software, GraphQL powers our most complex data-driven applications.
Start simple, let the schema evolve with client needs, and enjoy the productivity boost.
Building modern APIs with GraphQL and Laravel? Contact ZIRA Software to discuss how we can architect flexible, efficient GraphQL APIs for your application.