Sitemap

REST API vs GraphQL: A complete tutorial

Compare modern API design choices for flexibility, data fetching, scalability, and performance. Learn when to choose REST or GraphQL for your projects.

20 min readJan 23, 2025

--

Written by John Walter

Press enter or click to view image in full size

The ever-evolving world of web development has seen constant progress in APIs, a key strategy developers use to build scalable, robust, and flexible applications through standardized terminals for data exchange.

From seamless data workflows, application decoupling, code reusability, and app scalability to quick integrations, enhanced security, and incentivized innovation, APIs are vital in simplifying software development processes.

Though many API design styles exist, REST APIs and GraphQL are outstanding models.

REST APIs allow client applications to transact with servers using the Internet’s standard communication protocols (HTTP).

GraphQL defines blueprints for how client apps request data from servers. REST APIs have been around since the 2000s.

Although GraphQL has gained traction and significant command from the developer community sentiment because it can be used to make API calls without relying on the server to define requests, the popularity of REST APIs isn’t fading anytime soon. Both models are a contract between the front end and the back end. Coming from either of the backgrounds, it could be challenging to figure out the benefits of using one over the other.

In this article, we will compare GraphQL vs REST APIs side-by-side. Whether you are a seasoned developer or a learner exploring the world of APIs, we will give critical insights to help you understand both ecosystems. We’ll also spotlight when to use each. By the end, you’ll have enough knowledge to choose the right approach for building your projects. Let’s delve straight in!

Getting Started

Let’s see what it looks like to set up an API and compare GraphQL API vs REST API.

REST, short for Representation State Transfer, is a structured architecture for building APIs, also known as RESTful APIs, and consists of requests and responses between clients and servers. REST APIs use uniform resource identifiers (URIs) to address or manage resources.

Operations on REST APIs are done through different endpoints (GET, POST, PUT, DELETE) targeted at network resources to create, read, update, and delete actions, otherwise known as CRUD applications.

These operations are defined using standard HTTP.

To determine the size and shape of resources they provide to their clients, REST APIs rely on common data formats like JSON, XML, and YAML.

Though there are different takes on what RESTful API should satisfy, there are some common attributes.

The client and server should be separated in that the client handles the user interface and experience while the server stores, processes, and retrieves the data, a framework that improves scalability and portability.

All requests should hold all the information the server requires to process requests; at no point should the server store context about client sessions.

All data used for caching, authentication, and testing is found through request headers. Restful API responses can be defined as either cacheable or not, so the cacheable ones are reusable for subsequent similar requests. This technique can be used to reduce client-server interactions for better scalability.

Additionally, Restful APIs are layered systems.

By following a multi-layered architecture, each layer can provide specific functionalities.

A good example is an eCommerce API with separate levels: one for authentication, another for delivering product information, and one for processing orders. It also means that interactions aren’t affected if a proxy or load balancer is deployed between the client and the server.

Like visiting web pages, REST APIs should have uniformity and predictability — a uniform interface characteristic defined by HTTP conventions. As an optional constraint, RESTful APIs allow the server to extend a client’s functionality by sending executables such as JavaScript files, a feature known as code on demand.

Overview of GraphQL

As an open-source, GraphQL is an API runtime and query language that provides an understandable description of data.

It gives clients what they need and helps evolve APIs over time. Through plenty of powerful developer tools,

GraphQL exposes a single endpoint from which you can create, query, update, and delete data. In turn, this allows clients to request and receive the specific data they need without having to chain requests as REST APIs.

Using HTTP methods as the transport model, GraphQL API allows you to perform several actions: queries, mutations, and subscriptions. GraphQL queries give the client a declarative (defined data) schema that can fetch data from multiple sources and/or other APIs without making multiple trips to the server.

Mutations are about modifications on the server, such as the creation or updates on existing data objects.

For subscriptions, GraphQL supports real-time bi-directional interactions between clients and servers — a client can register for updates from the server based on events when published, and the server can do the same.

By grabbing only the required data, GraphQL allows clients to request data without over-fetching or under-fetching. It is also a standard gateway for unifying and hiding integrated systems with high-level complexity.

A good example is when migrating a monolithic backend application to a microservices-based one, GraphQL merges the various microservices into one schema. Once the schema is defined with all join services, the frontend and backend communicate seamlessly. Consequently, this structure simplifies and speeds up development.

Differences between GraphQL and REST API

There are many key factors to evaluate when choosing an API design. In this article, we will compare REST API and GraphQL based on the following:

  • Data fetching/handling
  • Versioning and Schema
  • Performance and Scalability
  • Error handling and response structure
  • Security
  • Caching

Data fetching mechanisms: fixed vs flexible

REST API

As a set of rules, REST APIs follow a convention such that you retrieve data (often called resources) when you link to a particular URL.

Each URL is a request, and the data sent back is a response. A REST API request comprises endpoints, methods, headers, and data (or body). Endpoints are paths, like those of web pages, that determine the resource you are requesting, with the root endpoint being the starting point of the API you’re interacting with.

When interacting with an API, you must read its documentation to understand the available paths. The final part of the path is query parameters, and while not part of the API architecture, they provide options to modify your request through key-value pairs and will begin with question marks.

In methods, the type of request sent to the server is specified from five options: GET, the default method for request data; POST, a resource creator; DELETE, a resource remover; and PUT and PATCH to update server requests.

Depending on the API, the request data can be defined in JSON format as a string or any other supported format for all the above methods, excluding the GET.

Headers provide information to the client and the server.

For example, if a request is sent to the server, the header will be the information used to authenticate and authorize operations. In the case of a server response to the client, it could be the information that dictates if the response is cacheable on the client side.

With plenty of resources in the API, so will the paths, leading to multiple endpoints and stateless interactions.

As such, every API request is processed as a unique query. In typical use cases, a client will always receive every piece of data tied to the specific resource, even if they only need a subset of the data, a scenario known as over-fetching.

At some point, a client will need data spanning multiple resources accessible by independently querying each path, under-fetching in this case. As explained, REST API data fetching can be defined as a fixed model.

Here are examples of under-fetching and over-fetching REST APIs. In the first snippet, the request provides a complete user profile, while we only need a few elements. Thus, we have to do some local filtering. In the second scenario, each post requires separate API calls for its comments, hence a more complex interaction. The promises.all() is used to fetch data in parallel requests, and the program will only proceed once all the comments are supplied in each post.

Over-fetching

Press enter or click to view image in full size

Under-fetching

Press enter or click to view image in full size

GraphQL

In GraphQL, you are exposed to only a single endpoint and can access all the required data through a single query. As such, every data field has a graphql.schema.DataFetcher tied to it. Most requests will take data from the memory objects using field names and Plain Old Java Objects (POJO). However, some will use specialized data fetchers (occasionally referred to as resolvers depending on the implementation) code that knows how to interact with a database. If a data fetcher is involved in the query, each is passed to a data-fetching-environment object containing the sought field and all other arguments needed in the context of that queried object.

By using the data-fetcher-objects following the `new DataFetcher() {…other parameters in here…}` convention, the API follows application security handlers to gain access to the database. Even without knowing how the fetcher environment works internally, you can still access the environment in your methods for customization through the graphql.schema.PropertyDataFetcher. This allows you to tweak the values before sending out requests. A good instance is the case of a date format as an optional argument to ensure your responses are in your predefined version.

The interesting bit about the data-fetching environment is that it has several key parts that are equivalent to the request-building elements in REST. They include the following methods:

  • getSource() — A method that gives access to the source object for the current field.
  • getRoot() — A root object to indicate the starting point for a GraphQL API.
  • getArguments() — Arguments passed into a field to resolve default values or variables.
  • getContext() — A context object shared throughout the query’s lifecycle.
  • getExecutionStepInfo() — A snapshot of the field’s type of information at the current execution step.
  • getSelectionSet() — A collection of fields selected under the selected field.
  • getExecutionId() — A unique execution ID to track individual queries.

The above parts enable the data fetcher to access essential data and metadata and tailor the logic based on the query context, requested fields, and provided arguments. Below is an example of a GraphQL request that uses the data fetcher and has access to the data fetcher environment. The structure is an example of a flexible data fetching mechanism.

Press enter or click to view image in full size

Versioning and schema

As your knowledge and system experience compound over time, changes in the API are inevitable, and handling the changes can be challenging because you do not want to break existing client applications that integrate your API.

In the GraphQL vs. REST API performance, APIs need to be versioned when semantic or breaking changes are made, including removal of API sections, changes in the format for one or many calls’ response data, and changes in the request or response type.

REST API

Commonly, there are four ways to version REST APIs. The first way is to version through the URI path by including the version number.

Major versions entail creating a new API, and the version number routes to the correct host. If it’s a minor or patch version, changes are communicated to the client through change logs to ensure backward compatible updates and inform about new functionalities or bug fixes.

As a result, clients can cache resources easily but at the cost of a big footprint on the API codebase, as entirely new branches have to be made.

Second, versioning through query parameters is straightforward since defaulting to the latest versions is easy.

As a tradeoff, it gets increasingly difficult to use routing requests for proper API versions.

Third, custom headers provide a way to version APIs. This means that the version number is included as an attribute on requests. By using custom headers, this method does not clutter the URI with versioning information. Lastly, APIs can be versioned through content negotiation, where resources vary independently.

Although this method provides granular version control by eliminating the need for URI rules, it requires headers to be armed with media types, making it difficult to test and explore the API on browsers.

The schema of REST APIs is implicit and often represented through documentation rather than an enforced structure.

You can use tools like OpenAPI/Swagger to define endpoints, parameters, request/response models, and data types to provide a formal structure. JSON Schema is also a good way to validate requests and payloads so that they conform to the expected formats.

Managing schema in REST involves documenting the inputs and output of all endpoints alongside their HTTP status codes. In this case, these external tools act as a blueprint to ensure consistency and ease in maintenance.

GraphQL

While no restriction limits you from version GraphQL APIs like REST, the approach here is that you have a continuously evolving GraphQL schema. Because GraphQL only provides explicitly sought data, new capabilities are added through new fields and types without creating breakage. In turn, you can serve a version-less API.

In GraphQL, breaking changes modify the signature of the schema, including field renaming, updating argument type for existing fields, adding new mandatory arguments to a field, or adding non-null data to a field response. As such, you can use versioning, but GraphQL encourages evolving schema.

Evolving the GraphQL schema requires that fields with breaking changes follow a convention by re-implementing a field with a different name, deprecating the old field, and asking clients to use the new field, and if a field is no longer used, it is removed. In practice, this approach potentially introduces some problems.

The updated schema could get more verbose and less understandable with the accumulation of deprecated fields being supported for some clients or multiple fields providing the same functionality.

Whether you use the code-first or schema-first approach to building your GraphQL, the problem can be solved by publicizing versions through warnings. For example, you can implement a mandatory version constraint, support the old version only for a particular timeline, or use the latest version and encourage users to state which version they’d like to use.

Performance & scalability

From the above presentation, it might seem like GraphQL is the superior API design model. While it offers advantages, this might not be the case in the performance context, and REST will be the preferred option in specific development scenarios.

For instance, caching is one way to improve API performance, and GraphQL doesn’t natively support browser and mobile cache. The same can be seen in error handling. GraphQL’s query complexity allows for nested calls, reducing the number of requests and leading to better performance.

An enforced schema in GraphQL provides data consistency, but adding new fields in Graph without affecting clients remains a challenge. The performance of either is deeply affected by data complexity, scalability needs, and development constraints. From such a posture, optimizing GraphQL vs. REST API performance would be the best way to accommodate each.

REST API

As applications scale, REST APIs are bound to encounter performance bottlenecks. The most common issues affecting REST APIs’ performance are network latency, needless data, business logic and data layer performance, security, and server-side load. Even with a good design and implementation of REST layers, the network speed drastically impacts the overall API responsiveness. Again, numerous paths are needed to serve many resources, and in the design, unwanted data could be sent back to the client (see the over-fetching scenario in data fetching. Poor design of the underlying business logic could slow the API performance. REST API layers run on top of HTTP protocols, and if their security is compromised through snooping or DDOS attacks, the overall results could be poor. Lastly, if the server does not have enough capacity to handle a high load, poor performance is inevitable with many client requests.

While the above limitations are well tackled, there are some techniques you could employ to optimize your REST-powered applications.

API Caching is one of the best ways to enhance the performance of REST APIs. This technique temporarily stores frequently accessed data for future requests for easy accessibility. Load balancing is a good option for scalability, handling increased traffic, and maintaining high availability. It distributes traffic across multiple servers, enhancing performance and fault tolerance.

In the implementation, multiple API instances can be achieved through physical servers, containerization, or virtual machines, depending on your infrastructure. In scaling strategies for future growth, containerization with Docker or orchestration with Kubernetes is a good practice.

Alternatively, you can adopt a microservices architecture. You may also want to control the number of requests clients can make on your API through rate limiting. This practice avoids API abuse, and the Token Bucket Algorithm in Java is a good example for implementation.

GraphQL

Like REST APIs, GraphQL models have factors that influence their performance, each with its impact, although network latency and underlying business logic will also stretch to them.

On this API type, the complexity of queries affects the performance by increasing the server’s work in the context of processing the request, which could lead to slow response times that directly impact user experience.

The data size on a query dictates some performance outcomes, as large ones require more processing time, and the response time increases.

Even if the client-side explicitly defines the requests for fetching data, the server side still has resolvers querying all the data on the server. Only by extracting the required fields from the resolver info object can GraphQL perform faster and avoid expensive queries. This means that more processing power is needed, but it is possible to scale the performance of GraphQL APIs.

To optimize performance in GraphQL, you should begin with the database schema and indexing.

Understanding how data is structured helps analyze relationships between tables, which in turn can improve the data access models. This start-off improves response time, reduces the load on the database, and reduces unnecessary trips to the database. Next, design efficient queries that only fetch the necessary data. If possible, prioritize critical data that applications need to function correctly.

Minimized data reduces processing load and speeds up response time. By writing efficient data resolvers, you can minimize the execution time and avoid query redundancy. Persisted queries are another method that entails storing predefined queries on the server with unique identifiers for their execution.

When implemented, persisted queries eliminate the need to parse and validate requests at runtime, improving the performance, a method best suited for high-traffic apps.

Lastly, you can implement data loaders for batching and caching. Many requests get clustered into a single one, and the results are temporarily stored for quick access.

Error handling and response structure

No matter your API architecture, all API errors need to be presented logically. Some common practices that cross REST API and GraphQL include providing a clear response structure for your error response, using descriptive error messages, avoiding sensitive data leaks, documenting common errors, and implementing logging and monitoring.

REST API

Typically, REST APIs represent errors using HTTP status codes, including 404 if a resource is not found and 500 for internal server errors, among others.

Depending on the nature of the error, the API provides an appropriate response for each scenario. Now that REST architectures are stateless, error messages provide the necessary information to clients for easy understanding without reliance on previous requests.

There’s also a consistent structure in fields: status, code, and message. Often, the response is an entire resource representation. This makes error handling straightforward but less flexible.

The flexibility is constrained by the fact that HTTP status codes aren’t that expressive and must fit into predefined categories.

The rigid separation of success and error response means there’s no way to handle partial errors in the case of multi-resource requests with a single failure point; you’ll either get the data in successful requests or an error object on failure. The code snippet below is an example of some REST API errors.

Press enter or click to view image in full size

GraphQL

Using a single endpoint for GraphQL means that errors are returned in the body response rather than HTTPS status codes. In this structure, both the data and error sections will be included. This criteria by GraphQL tries to make the response as useful as possible by capturing all the errors in an array that contains error objects.

Unlike REST APIs, which either give data as JSON or an error object, the GraphQL response carries data for successful results in a batched query, and errors will list the problematic fields. It also becomes easy to handle errors in granularity as extensions for metadata can be used to carry error codes and timestamps for more context.

Only with very complex queries could troubleshooting be harder. The response structure is still unified here.

In the example snippet below, we see that an error will have its code or message, and that’s a good starting point for debugging. It’s probably good enough for a client to resolve issues.

Press enter or click to view image in full size

Security concerns

In the security docket, neither REST API nor GraphQL is more secure than the other. Security heavily relies on how the application is designed, implemented, configured, and maintained, as both are architectural styles, each with its own considerations.

For instance, both REST and Graph need robust authentication mechanisms. While REST leverages traditional mechanisms like OAuth, JWT, and API keys at the endpoint level, GraphQL centralizes the process but poses a risk if not well handled.

Regarding data exposure, REST APIs have many endpoints that create more attack points when not well secured, while GraphQL leads to overexposure if proper controls are unapplied at the resolver levels.

Without input validation, REST APIs suffer SQL and NoSQL injection attacks, while GraphQL is vulnerable to injection queries if requests are parsed and validated correctly. As such, here’s how to ensure security for both API models.

REST API

Secure REST API implementations use Transport Layer Security (TSL) to encrypt the data from the server to the client while in transit.

When absent, a third party can intercept sensitive information like API credentials or client data. Most applications associate private data with a single client or user, so there’s a need to verify identities.

You can implement single sign-on (SSO) with OpenID by leveraging trusted third-party providers like Google, Microsoft Azure, or Amazon Web Services to handle this. API keys are another way to give users programmatic access to your data.

As a rule of thumb, encourage good secret management of API keys, choose when to enforce authorization with request levels, configure different permissions for different API keys, and ensure that the API authorization is handled by underlying application or business logic.

GraphQL

Authentication and authorization are no exemptions in GraphQL. TLS is also a good way to keep transitional data secure.

Advanced encryption algorithms (AES) will keep good security levels for data at rest. It is also a good practice to manage encryption keys with cloud providers or service providers of hardware security modules (HSMs).

In the error handling responses, you must ensure that the error object does not leak API internals.

If possible, adopting a zero-trust network model helps enforce strict verification and applies the least privilege principle. You’ll also want to control the depth of queries because multiple queries in GraphQL are defined in a single instance, which could be used to bypass limiting and potentially trigger harmful infinite loop requests.

Caching and real-time capabilities

REST API and GraphQL can be cached for real-time capabilities enhancement, though using different methods based on their architectures. REST relies on HTTP headers and URL structures, while GraphQL leverages schema and resolver-based techniques.

REST API

REST API supports caching for the client-side, and server-side, and a hybrid approach incorporating cache on both ends. Caching through an intermediary checkpoint like a content delivery network is also possible.

In client-side caching, API responses that comprise static assets like images and CSS files or runtime scripts are stored on the clients in local storage. Because the files won’t be fetched on the API upon subsequent requests, the API is alleviated from processing redundant requests.

Cache control is handled through headers. On the server side, frequent requests store their data on the server, and there’s no need to pull it from the database.

A good example is dynamic files that can be shared across multiple users or API calls that are expensive to execute.

GraphQL

Unlike REST APIs, where HTTP URLs are used as identifiers for similar resources, GraphQL provides a global identifier for each object that clients can use to build a cache. The object must be exposed to clients for use.

Alternatively, some implementations of GraphQL natively support caching. For example, client-side caching can be done using the Apollo client, one of the most popular implementations of GraphQL.

In other variants, the implementation will be the same but with a slight variance in the syntax. The Apollo client uses local memory to store cached responses and will only clear them if a page in the application is refreshed or reset using store reset methods.

In addition, the Apollo client supports mutations such that the cache is updated on changes in data from the server, and subsequent requests won’t have to re-fetch data from the database.

When to use REST API

No matter the technological progress introduced by GraphQL, there are reasons to opt for REST API in your applications. The learning curve for REST architectures is gentle if you’re entering the world of API development.

Among the advantages of using a REST API are that it allows you to scale software seamlessly, easy migrations from one server to another, smooth development across different projects, the opportunity to experiment with different environments while on development, clients have no need for routing information, explicit instance creation for processes, and quick to adaptation on building.

Choose to work with REST APIs if you’re building small applications with less complex data, need simplified query operations, or when most of your clients have similar data and operations. Moreover, consider REST if the resources or data you’re serving come from a single source or need the ability to upload files as part of your application through the PUT and POST methods. You may also want to apply REST when working with user interfaces (UIs), need a layered architecture with hierarchical layers, or work with applications built on multiple programming languages.

On the downside, REST APIs do not maintain states of previous interactions between client and server.

There is also no way to have limited fields, manipulate nested resources, and validate queries. You’d have to deal with poor search facilities, no standard tooling framework or guidance, and the API would have no way to manage additions, deprecations, or changes.

When to use GraphQL

If you work in a complex software environment, GraphQL can help you with flexible and efficient data fetching. It is also good for working with changing data requirements and providing client-driven solutions.

Consider using this model if your business has limited bandwidth and needs to limit API calls and responses, client requests vary significantly, and there’s a need to combine several data sources into a single endpoint. GraphQL is a good way to migrate applications from monolithic backends to mergeable microservices architecture.

Another advantage is that GraphQL keeps the documentation in sync with the codebase through a tight coupling that makes it easy to evolve APIs over time while supporting internal developers and external customers.

A list of the benefits of using GraphQL includes the following. The availability of a declarative query language, strongly typed queries run within the context of a particular use case or system encoded within the client, the ability to deal with multiple databases, batched queries, schema discovery in an appropriate format, rapid application prototyping, selective function exposure, and sharing fields through high component reusability.

But get ready to deal with a lack of resources on your backend, no design patterns for complex applications, performance issues with increased query complexity, and no HTTP methods to cache data, and it’d be an overkill for simple applications.

The bottom line

When deciding between the two API design models, we can narrow it down to a few lessons: For straightforward data requirements, fixed endpoints are a nice fit; hence, REST is the best.

Where complex data relationships are the base, catering to different clients, GraphQL is a good fit.

Dealing with multiple resources on a high latency network gravitates the decision to the GraphQL model, but caching to improve API performance urges you to use REST APIs.

Peeking into the developer marketplace, REST APIs have been around for a long, hence a well-established ecosystem for testing, documentation, and security models.

If your API is an ever-evolving product, GraphQL is the best option. REST is a good way to create CRUD applications in the context of data structures. On the other hand, GraphQL is good for managing complex and nested relationships.

Your choice between the two API architectural styles will depend on your problem and how you need to solve it.

Other resources to read:

--

--

Pieces 🌟
Pieces 🌟

Written by Pieces 🌟

Pieces is the world’s first micro-repo for developers — usable right inside from your IDE or browser. Created by developers for developers.

No responses yet