REST API Development

GraphQL vs REST

18 min Lesson 25 of 35

GraphQL vs REST

GraphQL and REST are two different approaches to building APIs. While REST has been the dominant standard for over a decade, GraphQL has emerged as a powerful alternative. Understanding when to use each—or even combining them—is crucial for modern API design.

Understanding REST

REST (Representational State Transfer) is an architectural style that uses HTTP methods and URLs to perform CRUD operations on resources.

REST Characteristics:
  • Resource-Based: Everything is a resource (users, posts, orders)
  • Multiple Endpoints: Each resource has its own endpoint
  • HTTP Methods: GET, POST, PUT, PATCH, DELETE
  • Fixed Responses: Server determines response structure
  • Stateless: Each request contains all necessary information
// REST API Examples // Get all users GET /api/v1/users // Get specific user GET /api/v1/users/123 // Get user's posts GET /api/v1/users/123/posts // Get specific post GET /api/v1/posts/456 // Get post's comments GET /api/v1/posts/456/comments // Response: Fixed structure determined by server { "data": { "id": 123, "type": "users", "attributes": { "name": "John Doe", "email": "john@example.com", "created_at": "2024-01-01T00:00:00Z", "bio": "Developer", "avatar_url": "https://...", "followers_count": 150, "following_count": 75 } } }

Understanding GraphQL

GraphQL is a query language for APIs that allows clients to request exactly the data they need, nothing more, nothing less.

GraphQL Characteristics:
  • Single Endpoint: Typically one endpoint (e.g., /graphql)
  • Client-Driven: Client specifies exact data requirements
  • Strongly Typed: Schema defines all available operations
  • No Over/Under-fetching: Get precisely what you ask for
  • Introspection: Schema is self-documenting
# GraphQL Query Examples # Get specific user fields only query { user(id: 123) { name email } } # Response: Only requested fields { "data": { "user": { "name": "John Doe", "email": "john@example.com" } } } # Get user with nested relationships in ONE request query { user(id: 123) { name email posts(limit: 5) { id title createdAt comments(limit: 3) { id text author { name } } } } } # Mutations (write operations) mutation { createPost(input: { title: "My New Post" content: "Post content here" }) { id title createdAt } }

Setting Up GraphQL in Laravel

# Install Lighthouse (Laravel GraphQL library) composer require nuwave/lighthouse # Publish configuration php artisan vendor:publish --tag=lighthouse-config # Publish default schema php artisan vendor:publish --tag=lighthouse-schema
# graphql/schema.graphql "User type definition" type User { id: ID! name: String! email: String! posts: [Post!]! @hasMany createdAt: DateTime! } "Post type definition" type Post { id: ID! title: String! content: String! author: User! @belongsTo comments: [Comment!]! @hasMany createdAt: DateTime! updatedAt: DateTime! } "Comment type definition" type Comment { id: ID! text: String! author: User! @belongsTo post: Post! @belongsTo createdAt: DateTime! } "Root Query type" type Query { "Get all users with optional filtering" users( name: String @where(operator: "like") orderBy: [OrderByClause!] @orderBy ): [User!]! @paginate(defaultCount: 10) "Get single user by ID" user(id: ID! @eq): User @find "Get authenticated user" me: User @auth "Get all posts" posts( title: String @where(operator: "like") orderBy: [OrderByClause!] @orderBy ): [Post!]! @paginate(defaultCount: 20) "Get single post" post(id: ID! @eq): Post @find } "Root Mutation type" type Mutation { "Create a new post" createPost(input: CreatePostInput! @spread): Post @create @inject(context: "user.id", name: "user_id") "Update existing post" updatePost(id: ID!, input: UpdatePostInput! @spread): Post @update "Delete a post" deletePost(id: ID! @eq): Post @delete "Login user" login(email: String!, password: String!): AuthPayload! } "Input for creating posts" input CreatePostInput { title: String! @rules(apply: ["required", "max:255"]) content: String! @rules(apply: ["required"]) } "Input for updating posts" input UpdatePostInput { title: String @rules(apply: ["max:255"]) content: String } "Authentication payload" type AuthPayload { token: String! user: User! } "Pagination information" type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
<?php // app/GraphQL/Mutations/Login.php namespace App\GraphQL\Mutations; use App\Models\User; use Illuminate\Support\Facades\Hash; use GraphQL\Error\Error; class Login { public function __invoke($rootValue, array $args): array { $user = User::where('email', $args['email'])->first(); if (!$user || !Hash::check($args['password'], $user->password)) { throw new Error('Invalid credentials'); } $token = $user->createToken('api-token')->plainTextToken; return [ 'token' => $token, 'user' => $user ]; } }

Key Differences: REST vs GraphQL

1. Data Fetching

// REST: Multiple requests for nested data // Request 1: Get user GET /api/v1/users/123 // Response: Full user object with many unused fields // Request 2: Get user's posts GET /api/v1/users/123/posts // Response: Array of posts with all fields // Request 3: Get comments for each post GET /api/v1/posts/1/comments GET /api/v1/posts/2/comments GET /api/v1/posts/3/comments // Total: 5+ HTTP requests, lots of unnecessary data // --- // GraphQL: Single request for nested data query { user(id: 123) { name email posts(limit: 3) { title comments(limit: 2) { text author { name } } } } } // Total: 1 HTTP request, only requested fields

2. Over-fetching and Under-fetching

REST Problems:
  • Over-fetching: Getting more data than needed (e.g., full user object when you only need name)
  • Under-fetching: Not getting enough data, requiring additional requests
GraphQL Solution:

Client requests exactly what it needs—no more, no less.

3. Versioning

// REST: Typically uses URL versioning GET /api/v1/users/123 GET /api/v2/users/123 // New version with different response // Problem: Maintaining multiple versions // - /api/v1/users // - /api/v2/users // - /api/v3/users // --- // GraphQL: Schema evolution without versioning type User { id: ID! name: String! email: String! fullName: String! @deprecated(reason: "Use 'name' instead") posts: [Post!]! articles: [Post!]! @deprecated(reason: "Use 'posts' instead") } // Add new fields without breaking existing queries // Deprecate old fields gracefully // Single endpoint evolves over time

4. Error Handling

// REST: HTTP status codes // 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Server Error { "errors": [{ "status": "422", "title": "Validation Error", "detail": "Email is required", "source": { "pointer": "/data/attributes/email" } }] } // --- // GraphQL: Always returns 200, errors in response body { "data": { "createPost": null }, "errors": [ { "message": "Validation error", "extensions": { "validation": { "title": ["The title field is required"], "content": ["The content field is required"] } }, "path": ["createPost"] } ] } // Can return partial data with errors { "data": { "user": { "name": "John Doe", "posts": null // Failed to load } }, "errors": [ { "message": "Failed to load posts", "path": ["user", "posts"] } ] }

When to Use REST

REST is Better When:
  • Simple CRUD operations: Straightforward resource management
  • Caching is critical: HTTP caching works out of the box
  • File uploads/downloads: REST handles binary data more naturally
  • Team familiarity: Team knows REST well
  • Third-party integrations: Many services expect REST
  • Public APIs: REST is more widely understood
  • Microservices: Internal service-to-service communication

When to Use GraphQL

GraphQL is Better When:
  • Complex data requirements: Nested relationships and flexible queries
  • Mobile applications: Minimize bandwidth and requests
  • Rapid iteration: Frontend can request new fields without backend changes
  • Multiple clients: Different clients need different data shapes
  • Real-time updates: GraphQL subscriptions for live data
  • Developer experience: Self-documenting schema, excellent tooling
  • Aggregation: Combining data from multiple sources

Hybrid Approaches

You don't have to choose one over the other. Many companies use both:

<?php // Hybrid API: REST + GraphQL // routes/api.php // REST endpoints for simple operations Route::prefix('v1')->group(function () { Route::apiResource('users', UserController::class); Route::post('login', [AuthController::class, 'login']); Route::post('logout', [AuthController::class, 'logout']); // File uploads via REST Route::post('upload', [UploadController::class, 'store']); }); // GraphQL endpoint for complex queries Route::post('/graphql', [GraphQLController::class, 'handle']); // GraphQL playground in development if (app()->environment('local')) { Route::get('/graphql-playground', [GraphQLController::class, 'playground']); }

Migration Strategies

Migrating from REST to GraphQL

<?php // Gradual migration approach // Phase 1: Add GraphQL alongside REST // - Keep all REST endpoints // - Add GraphQL endpoint // - Let clients choose // Phase 2: Implement GraphQL resolvers using existing logic class UserResolver { public function __invoke($root, array $args) { // Reuse existing REST controller logic return app(UserController::class)->show($args['id']); } } // Phase 3: Deprecate REST endpoints gradually Route::prefix('v1')->group(function () { // Mark as deprecated in documentation Route::get('users/{id}', [UserController::class, 'show']) ->middleware(AddDeprecationHeader::class); }); // Phase 4: Monitor usage and remove unused endpoints // - Track API usage metrics // - Notify clients of deprecation timeline // - Remove endpoints with zero usage

Adding REST to GraphQL

<?php // Expose GraphQL queries as REST endpoints Route::get('/api/users/{id}', function ($id) { $query = ''' query GetUser($id: ID!) { user(id: $id) { id name email posts { id title } } } '''; $result = GraphQL::query($query, ['id' => $id]); return response()->json($result); }); // This gives you REST-like endpoints with GraphQL power underneath

Performance Considerations

GraphQL N+1 Problem:

GraphQL can cause N+1 queries if not careful:

# Query that causes N+1 problem query { posts { # 1 query: SELECT * FROM posts id title author { # N queries: SELECT * FROM users WHERE id = ? name # (one query per post!) } } } # Solution 1: DataLoader (batching) # Solution 2: Eager loading in resolvers <?php // Use Laravel's eager loading class PostResolver { public function __invoke() { return Post::with('author')->get(); // Single query with JOIN } } // Or use Lighthouse's @with directive type Query { posts: [Post!]! @all @with(relation: "author") }

Comparison Summary

┌─────────────────────┬──────────────────────────┬──────────────────────────┐ │ Feature │ REST │ GraphQL │ ├─────────────────────┼──────────────────────────┼──────────────────────────┤ │ Endpoints │ Multiple │ Single │ │ Data Fetching │ Fixed by server │ Specified by client │ │ Over-fetching │ Common │ None │ │ Under-fetching │ Common │ None │ │ Versioning │ URL-based (v1, v2) │ Schema evolution │ │ Caching │ HTTP caching built-in │ Requires implementation │ │ Learning Curve │ Low │ Medium-High │ │ Tooling │ Mature │ Excellent and growing │ │ File Uploads │ Natural │ More complex │ │ Real-time │ WebSockets/SSE │ Subscriptions │ │ Discoverability │ Documentation needed │ Introspection built-in │ │ Mobile-friendly │ Can be inefficient │ Very efficient │ └─────────────────────┴──────────────────────────┴──────────────────────────┘
Exercise:
  1. Build the same API feature using both REST and GraphQL
  2. Compare the number of requests needed to fetch nested data
  3. Implement a GraphQL schema with relationships
  4. Add authentication to both REST and GraphQL endpoints
  5. Measure performance differences between the two approaches
  6. Design a migration strategy from REST to GraphQL for an existing project
  7. Implement DataLoader to solve the N+1 problem in GraphQL
The Verdict:

Neither REST nor GraphQL is universally better. The right choice depends on your specific use case:

  • Use REST for simple, cacheable APIs with well-defined resources
  • Use GraphQL for complex data requirements and multiple client types
  • Use both if you need the strengths of each for different use cases

Many successful companies use hybrid approaches, leveraging REST for simple operations and GraphQL for complex queries.