DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

REST vs GraphQL: Key Differences, Examples, and When to Use Each

Choosing the right API architecture can make a significant difference in how your application performs, scales, and evolves. REST and GraphQL are two popular approaches to building APIs, and both can power everything from small applications to complex enterprise systems.

REST has been the standard choice for web APIs for years, offering resource-based endpoints and predictable HTTP conventions. GraphQL introduces a different approach by allowing clients to request exactly the data they need through a single endpoint.

Understanding their differences is essential for JavaScript developers working with Node.js, React, mobile applications, and distributed systems. In this article, we will explore how both approaches work, implement practical JavaScript examples, and identify when each one makes the most sense.

REST vs GraphQL: Understanding the Core Differences

REST, or Representational State Transfer, is an architectural style that organizes an API around resources. Each resource typically has its own URL, and HTTP methods such as GET, POST, PATCH, and DELETE describe the operations clients can perform. For example, a REST API might expose /api/users/42 to retrieve a user and /api/users/42/orders to retrieve that user's orders.

GraphQL is a query language for APIs and a runtime for executing those queries against a defined schema. Instead of requiring a separate endpoint for every resource, a GraphQL server commonly exposes a single endpoint such as /graphql. Clients specify the fields they need, while resolvers determine how the server retrieves those fields from databases, services, or other data sources.

One important difference is data fetching. With REST, a client may need multiple requests to retrieve a user and their related orders, depending on the API design. GraphQL can fetch both through one query, reducing client-side request coordination, although the server must still manage database queries, authorization, and performance carefully.

REST benefits from established HTTP semantics, straightforward caching strategies, and broad infrastructure support. GraphQL provides flexible queries, a strongly typed schema, and useful introspection and developer tooling. However, GraphQL can introduce complexity around query depth, query cost, resolver efficiency, and caching, while REST can require additional endpoints or response-shaping strategies for clients with different data needs.

Consider a dashboard that displays a user's name and their recent orders. A REST implementation might request /api/users/42 and /api/users/42/orders?limit=3. A GraphQL implementation could request the user and three orders together, returning only the selected fields. Neither approach is automatically faster: actual performance depends on network conditions, server implementation, database access, payload size, and caching.

For a practical comparison, the following runnable JavaScript example uses Node.js's built-in HTTP module. It implements a small REST endpoint and a simplified GraphQL-style query endpoint using the same in-memory data, allowing you to compare resource-based requests with field selection without installing external packages. The GraphQL-style endpoint demonstrates the core idea but is not a complete GraphQL implementation because it does not parse or validate GraphQL syntax or provide a schema-based execution engine.

const http = require('node:http');
const { URL } = require('node:url');

// Sample data shared by both API styles.
const users = [
  {
    id: '42',
    name: 'Ansh',
    email: 'ansh@example.com',
    orders: [
      { id: '101', total: 499, status: 'shipped' },
      { id: '102', total: 899, status: 'processing' },
      { id: '103', total: 299, status: 'delivered' },
      { id: '104', total: 199, status: 'pending' }
    ]
  }
];

// Find a user by ID, returning undefined when no match exists.
function findUser(id) {
  return users.find((user) => user.id === id);
}

// Read the request body safely for JSON-based POST requests.
function readJsonBody(req) {
  return new Promise((resolve, reject) => {
    let body = '';

    req.on('data', (chunk) => {
      body += chunk;

      // Prevent unbounded request bodies in this demonstration.
      if (body.length > 100_000) {
        reject(new Error('Request body is too large'));
        req.destroy();
      }
    });

    req.on('end', () => {
      try {
        resolve(body ? JSON.parse(body) : {});
      } catch {
        reject(new Error('Invalid JSON body'));
      }
    });

    req.on('error', reject);
  });
}

// Write a JSON response with an appropriate HTTP status.
function sendJson(res, statusCode, data) {
  res.writeHead(statusCode, {
    'Content-Type': 'application/json; charset=utf-8',
    'Cache-Control': 'no-store'
  });
  res.end(JSON.stringify(data));
}

// REST handler: each URL represents a resource.
function handleRest(req, res, url) {
  const userMatch = url.pathname.match(/^\/api\/users\/([^/]+)$/);
  const ordersMatch = url.pathname.match(/^\/api\/users\/([^/]+)\/orders$/);

  if (req.method !== 'GET') {
    return sendJson(res, 405, { error: 'Method not allowed' });
  }

  if (userMatch) {
    const user = findUser(userMatch[1]);

    if (!user) {
      return sendJson(res, 404, { error: 'User not found' });
    }

    console.log('[REST] Returning the user resource');
    return sendJson(res, 200, {
      id: user.id,
      name: user.name,
      email: user.email
    });
  }

  if (ordersMatch) {
    const user = findUser(ordersMatch[1]);

    if (!user) {
      return sendJson(res, 404, { error: 'User not found' });
    }

    // Support a bounded limit to keep the response predictable.
    const requestedLimit = Number(url.searchParams.get('limit') || 3);
    const limit = Number.isInteger(requestedLimit)
      ? Math.max(1, Math.min(requestedLimit, 10))
      : 3;

    console.log('[REST] Returning the orders resource');
    return sendJson(res, 200, {
      orders: user.orders.slice(0, limit)
    });
  }

  return sendJson(res, 404, { error: 'Route not found' });
}

// A deliberately simplified GraphQL-style field-selection handler.
// This is NOT a full GraphQL parser or production GraphQL server.
async function handleGraphql(req, res) {
  if (req.method !== 'POST') {
    return sendJson(res, 405, { error: 'Use POST for this demo endpoint' });
  }

  let body;

  try {
    body = await readJsonBody(req);
  } catch (error) {
    return sendJson(res, 400, { error: error.message });
  }

  // The demo accepts a JSON selection object instead of GraphQL syntax.
  const { userId, fields } = body;

  if (typeof userId !== 'string' || !Array.isArray(fields)) {
    return sendJson(res, 400, {
      error: 'Provide userId as a string and fields as an array'
    });
  }

  const user = findUser(userId);

  if (!user) {
    return sendJson(res, 200, {
      data: { user: null },
      errors: [{ message: 'User not found' }]
    });
  }

  // Only allow explicitly supported fields to be selected.
  const allowedFields = new Set(['id', 'name', 'email', 'orders']);
  const invalidFields = fields.filter((field) => !allowedFields.has(field));

  if (invalidFields.length > 0) {
    return sendJson(res, 400, {
      error: `Unsupported fields: ${invalidFields.join(', ')}`
    });
  }

  const selectedUser = {};

  for (const field of fields) {
    // Return only the fields requested by the client.
    if (field === 'orders') {
      selectedUser.orders = user.orders.map(({ id, total, status }) => ({
        id,
        total,
        status
      }));
    } else {
      selectedUser[field] = user[field];
    }
  }

  console.log('[GraphQL-style] Returning selected user fields');
  return sendJson(res, 200, { data: { user: selectedUser } });
}

// Route incoming requests to the appropriate API handler.
const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost:3000');

  try {
    if (url.pathname.startsWith('/api/')) {
      return handleRest(req, res, url);
    }

    if (url.pathname === '/graphql') {
      return await handleGraphql(req, res);
    }

    return sendJson(res, 404, { error: 'Endpoint not found' });
  } catch (error) {
    console.error('[Server] Unexpected error:', error.message);

    if (!res.headersSent) {
      return sendJson(res, 500, { error: 'Internal server error' });
    }

    res.destroy();
  }
});

// Start the local server and display example requests.
server.listen(3000, () => {
  console.log('API server running at http://localhost:3000');
  console.log('REST user: GET http://localhost:3000/api/users/42');
  console.log('REST orders: GET http://localhost:3000/api/users/42/orders?limit=3');
  console.log('GraphQL-style: POST http://localhost:3000/graphql');
  console.log('Send JSON: {"userId":"42","fields":["name","orders"]}');
});

// Stop the server cleanly when the process receives a termination signal.
process.on('SIGINT', () => {
  console.log('\nShutting down the API server...');
  server.close(() => {
    console.log('Server closed successfully.');
    process.exit(0);
  });
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

REST and GraphQL solve the same fundamental problem: allowing applications to exchange data through well-defined APIs. REST is often a strong choice when resources map naturally to endpoints, HTTP caching is important, and simplicity is a priority. GraphQL can be especially useful when multiple clients need different combinations of related data, such as React web applications and React Native mobile apps.

The best decision depends on your application's requirements rather than which technology is more popular. Consider team experience, data relationships, caching, authorization, monitoring, and the operational complexity of each approach before choosing.

If you are building a straightforward CRUD application, start with REST unless you have a concrete reason to introduce GraphQL. If your clients repeatedly need different shapes of interconnected data, evaluate GraphQL with careful attention to resolver performance, query limits, and security. Which approach are you currently using in your JavaScript projects, and what has influenced your decision?

Top comments (0)