A Comprehensive Guide To RESTful Simplicity vs GraphQL Flexibility

Vessel
16 min readAug 9, 2023

REST APIs rule the world. Pretty much every service on the modern web relies on REST APIs.

Yet, as the web grows more complex and the demand for more efficient data handling increases, REST APIs, with their unassuming simplicity, begin to show some strain. While they have proven their worth in myriad applications, providing a reliable way to create, read, update, and delete data, they also have their share of limitations. Over-fetching, under-fetching, and the time-consuming necessity to hit multiple endpoints for intricate data needs are just a few examples.

Enter GraphQL: a powerful alternative that offers not just a way to fetch data, but a complete query language that revolutionizes how clients ask for and receive information. Unlike REST, GraphQL allows clients to dictate exactly what data they need, reducing unnecessary network requests and providing immense flexibility. Yet, this power comes with its own complexities and learning curve. The question then isn’t about whether REST or GraphQL is universally better, but about which tool is right for the job at hand.

What are REST APIs?

The key idea behind REST APIs is simplicity.

REST APIs were designed as a response to the standard APIs of the time (the late 1990s), such as SOAP APIs. SOAP (Simple Object Access Protocol) APIs worked well for exchanging data between the enterprise software systems of the day. But as the web started to dominate software and tech, SOAP just wasn’t a viable solution.

SOAP isn’t simple. It is based on a simple idea, XML, but it requires a substantial amount of XML configuration. It can be difficult to write and understand, leading to longer development times and greater possibility for errors.

It also wasn’t optimized for the web. It has poor performance as SOAP uses XML exclusively for its message format. XML is a verbose and heavy data format, resulting in large message sizes. This can lead to network, storage, and processing overhead, which in turn can result in poor performance and higher latency. It is also not particularly browser-friendly. SOAP is not designed to use a URL as a means to directly access resources. This makes it less compatible with modern web technologies, as it can’t leverage native browser features like caching.

So in 2000, in his doctoral dissertation, computer scientist Roy Fielding introduced the idea of REST APIs, an architectural style for distributed hypermedia systems. REST stands for REpresentational State Transfer, which means we are transferring the state of ‘representations’ from one place to another (a client to a server, and vice versa), where a ‘representation’ basically means some type of data that can be encoded, such as a customer record or a blog post.

This was a simpler, more efficient means of creating APIs. It is simpler because:

  • It is stateless: REST is stateless, meaning the server does not keep any data (state) between two requests.
  • It leans heavily on HTTP. REST APIs leverage standard HTTP methods (GET, POST, PUT, PATCH, and DELETE) to interact with resources. REST APIs also use HTTP status codes to indicate the status of the request, such as 200 for “OK,” 404 for “Not found,” and 400 for “Bad Request.” REST APIs can also use in-built caches in clients to improve performance.

You should read the dissertation if you have time, but here are the other key components of REST APIs Fielding laid out:

  • Resource: Each entity in the system that is exposed via the API is considered a “Resource.” They are identified by logical URLs. For example, in an e-commerce system, the resources might be users, products, and orders.
  • Representation: When a client retrieves a resource, the server sends a “representation” of the resource, typically in XML or JSON format. When a client creates or updates a resource, it sends a representation to the server.
  • Uniform Interface: The principle of having a uniform interface helps to decouple the client from the server, allowing each to be developed and evolve independently. This includes using resource identifiers (URI), resource manipulation through representations, self-descriptive messages, and hypermedia as the engine of application state (HATEOAS).
  • Cacheability: Responses from the server can be cached by the client to improve performance.

An additional simplicity factor is that the HTTP methods REST APIs use map well to the create, read, update, and delete (CRUD) operations on resources in databases. This makes it extremely easy for developers to control data through creating REST APIs.

Let’s show how simple these REST APIs can be, starting with a GET request. We’re use the JSONPlaceholder API, which is a simple fake REST API for testing and prototyping:

async function getPosts() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

getPosts();

In this example, getPosts is an async GET call that fetches posts from the JSONPlaceholder API. When called, it waits for the fetch function to complete, then waits for the response to be converted to JSON, and then logs the data to the console. GET is the only REST API that doesn’t require any additional data to be sent to the server.

Here’s an example of a POST request:

async function createPost() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify({
title: 'My New Post',
body: 'This is the content of my new post.',
userId: 1
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

createPost();

In this example, createPost is a POST function that creates a new post on the JSONPlaceholder API. It does this by passing an options object as the second argument to the fetch function, which specifies the HTTP method to use (POST), the body of the request (a JSON string containing the new post’s data), and the headers for the request (to specify the content type of the request body).

PUT and PATCH are two HTTP methods that are commonly used for updating resources in REST APIs. However, they are used in slightly different ways:

  • PUT: This method is used to update an entire resource with the new supplied values. If you only supply some fields, the unspecified fields may be cleared or set to their default values (this behavior depends on how the API is implemented). If the resource does not exist, a PUT request can also be used to create it.
  • PATCH: This method is used for partial updates. That is, you only need to provide the fields that you want to change. Unspecified fields will retain their current values.

Here’s some code to illustrate the differences:

async function updateUserPut() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1', {
method: 'PUT',
body: JSON.stringify({
name: 'Updated User',
email: 'updateduser@example.com'
// other fields left out intentionally
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

updateUserPut();

In this PUT example, we’re updating the user with ID 1. Only the name and email fields are supplied. All other fields will be set to their default values or cleared.

Here’s a PATCH:

async function updateUserPatch() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1', {
method: 'PATCH',
body: JSON.stringify({
email: 'updateduser@example.com'
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

updateUserPatch();

In this PATCH example, we’re only updating the email field of the user with ID 1. All other fields will retain their current values.

A DELETE request obviously deletes data:

async function deleteUser() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1', {
method: 'DELETE'
});

// A successful DELETE request is expected to return HTTP status 200
if (response.status == 200) {
console.log('User deleted successfully');
} else {
console.log('Failed to delete the user');
}
} catch (error) {
console.error('Error:', error);
}
}

deleteUser();

In this example, deleteUser is a function that sends a DELETE request to delete the user with ID 1. It does this by passing an options object as the second argument to the fetch function, which specifies the HTTP method to use (‘DELETE’).

That’s it. REST APIs really are that simple. Obviously, different implementations of them can lead to massively different data needing to be sent to the APIs in the body, but you are doing everything with just these five types of request.

Overall, the good things about REST APIs are:

  • Scalability: Because REST APIs are stateless, they inherently allow for scalability as there’s no need to retain any client session on the server.
  • Simplicity: REST uses standard HTTP methods, which makes it simple to understand and use.
  • Performance: The ability to cache responses improves the performance of REST APIs.
  • Stateless: Since REST is stateless, the server doesn’t need to store any context about the client session.

But they have drawbacks. These mainly come from the simplicity of REST API design. Since the user has little control over what data is returned, it can lead to overfetching data or underfetching data.

Overfetching happens when the client downloads more information than it actually needs. In a REST API, endpoints usually return a fixed data structure. If the client only needs a portion of the data, there’s no easy way to tell the server to send only that required data.

Let’s say we have a REST API for a blog, and we want to display a list of blog post titles. The endpoint /api/posts might return something like this for each post:

{
"id": 1,
"title": "My first blog post",
"content": "Lots of content...",
"author": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
}

But to display the list, we only need the id and title of each post. All the other data (content, author id, name, email) is not used, which means we’re overfetching data.

Underfetching occurs when an API endpoint doesn’t provide enough of the required information. The client has to make additional requests to fetch everything it needs.

Suppose we have an endpoint /api/posts that only returns the id and title of each post:

{
"id": 1,
"title": "My first blog post"
}

Now, if we want to display the full content of a blog post along with the author’s name, we have to make additional requests to /api/posts/:id for the content and possibly /api/authors/:id for the author’s name. This is underfetching, which can result in multiple round-trips to the server and negatively impact performance.

First, the client makes a GET request to /api/posts to get a list of all posts. The server responds with something like:

[
{
"id": 1,
"title": "My first blog post"
},
{
"id": 2,
"title": "My second blog post"
}
]

The user clicks on the first post. Now, the client has to make another GET request, this time to /api/posts/1, to get the full content of the post. The server responds with something like:

{
"id": 1,
"title": "My first blog post",
"content": "Lots of content...",
"authorId": 1
}

Now we have the post content, but we still don’t have the author’s name and email. So the client has to make a third GET request, this time to /api/authors/1. The server responds with something like:

{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}

So, to display one blog post with its author information, the client has to make three separate requests.

This leads to performance issues, especially when dealing with a large number of resources or when the client has a slow network connection.

And this is exactly what GraphQL was built to address.

What are GraphQL APIs?

GraphQL is a query language for APIs and a runtime for executing those queries with your existing data, developed internally by Facebook in 2012 before being publicly released in 2015.

The primary advantage of GraphQL is that it allows clients to define the structure of the responses they want to receive. This prevents the overfetching and underfetching seen in REST APIs.

Here are some key concepts in GraphQL:

  • Schema: A GraphQL schema is at the heart of any GraphQL server implementation and describes the functionality available to the clients which connect to it.
  • Type System: GraphQL uses a type system to describe the data that’s possible to query in the API. For each type, you define fields that expose data and can also have their own sets of fields.
  • Queries: Queries in GraphQL are the equivalent of GET requests in REST. They allow the client to specify the data needed, preventing over-fetching or under-fetching. Importantly, they can get multiple resources in a single request, and they can also get related resources.
  • Mutations: In GraphQL, changes to data on the server are made using mutations, which are similar to PUT, POST, PATCH or DELETE in REST. The client specifies the data to be changed, and the data to be returned by the mutation.

There are also two other important parts of GraphQL. Resolvers define the instructions for turning a GraphQL operation into data and help the GraphQL server to know how to find the data for each field in a query. Subscriptions are real-time updates in GraphQL. When a query is made with a subscription, the server sends a response to the client whenever a particular event happens, akin to webhooks. We won’t cover these here as they are more advanced concepts in GraphQL.

Let’s start with the schema. Here’s an example of a GraphQL schema, for a simple blog application. We can also see the type system at play:

type Query {
posts: [Post]
post(id: ID!): Post
author(id: ID!): Author
}

type Mutation {
createPost(title: String!, content: String!, authorId: ID!): Post
updatePost(id: ID!, title: String, content: String): Post
deletePost(id: ID!): Post
}

type Post {
id: ID!
title: String
content: String
author: Author
}

type Author {
id: ID!
name: String
email: String
posts: [Post]
}

In this schema:

  • Query and Mutation are the entry points for the operations the clients can execute. Query is for fetching data (GET equivalent in REST), and Mutation is for changing data (POST, PUT, PATCH, DELETE equivalents in REST).
  • Post and Author are object types representing a blog post and an author, respectively. They define the shape of the data objects.
  • Fields like id, title, and content are scalar types, which represent the leaves of the graph. The ! after a type means that the field is non-nullable.
  • The square brackets around [Post] mean that posts is a list of Post objects.
  • The author field in the Post type and the posts field in the Author type create a relationship between the Post and Author types.

This schema provides a strong contract for the client-server communication, and any data queries or mutations sent by a client will be validated and executed against this schema.

Let’s work through an example with a hypothetical GraphQL API endpoint at https://myapi.com/graphql. We’ll start with the GET request equivalent, a query:

async function getPosts() {
const query = `
query {
posts {
id
title
content
author {
id
name
email
}
}
}
`;

try {
const response = await fetch('https://myapi.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});

const { data } = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

getPosts();

This will query the database for the id, title, content, and author of all the posts.

Aside: Even though it’s a different type of operation, the HTTP method is still POST for all GraphQL requests. This is because the type of operation is encoded in the body of the request, not the HTTP method. Another option for communicating with a GraphQL endpoint is to use a specific library, such as Apollo Client, that is optimized for GraphQL.

Now let’s use a Mutation. Here we’re going to use createPost and pass it the title, the content, and an authorId:

async function createPost() {
const mutation = `
mutation {
addPost(title: "New Post", content: "New Content", authorId: "1") {
id
title
content
}
}
`;

try {
const response = await fetch('https://myapi.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: mutation })
});

const { data } = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

createPost();

We can use the updatePost mutation to update the title and content of the Post with id 1:

async function updatePost() {
const mutation = `
mutation {
updatePost(id: "1", title: "Updated Title", content: "Updated Content") {
id
title
content
}
}
`;

try {
const response = await fetch('https://myapi.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: mutation })
});

const { data } = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

updatePost();

Whereas with REST APIs you have two different methods, PUT and PATCH to perform a full update or a partial update, respectively, GraphQL isn’t based on HTTP methods so these aren’t available. Instead, in GraphQL, whether a field is updated or not, depends on if you include it in the mutation. We have to write separate mutations for these, such as this mutation to just update the title:

async function updatePostTitle() {
const query = `
mutation {
updatePost(id: "1", input: {title: "Updated Post Title"}) {
id
title
content
}
}
`;

try {
const response = await fetch('https://myapi.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});

const { data } = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

updatePostTitle();

Finally, we can use the deletePost mutation:

async function deletePost() {
const mutation = `
mutation {
deletePost(id: "1") {
id
}
}
`;

try {
const response = await fetch('https://myapi.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: mutation })
});

const { data } = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}

deletePost();

With these few examples, you can start to see the flexibility of GraphQL. You can ask for any data you need without having to call multiple endpoints or do round-trips. You can also write the exact mutations you need for your CRUD applications.

So, GraphQL is the future, right?

Why REST is almost always going to be the right answer

We can see from these examples some of the advantages of GraphQL over REST. When you make a mutation, you can specify the data you want to get back. This allows you to retrieve the updated data in the same request as the mutation–no need for another GET request to grab the data you only just added or changed, so fewer round-trips. This means you get Goldilocks fetching–not too much, not too little. Just right.

Two other bonuses of GraphQL are:

  • Strong Typing: GraphQL is inherently strongly typed. This means that the API’s schema defines the shape and type of the data that can be queried, and the server validates every query against this schema. This leads to enhanced error checking at compile-time and helps eliminate many common data-related bugs. This also facilitates autocompletion, validation, and more in IDEs and editor setups, significantly improving the developer experience and productivity.
  • Good for Lots of Data: GraphQL is excellent when dealing with large and complex datasets. This is largely due to its ability to allow the client to request for exactly what it needs, avoiding unnecessary data transfer. This is particularly beneficial in applications where bandwidth or resource usage is a concern. It also supports batch operations, allowing you to bundle multiple operations into a single request, thus reducing the overall load and enhancing the efficiency of the server. Additionally, it can simplify dealing with complex, nested data structures, making it easier to work with data-rich applications.

BUT. All this comes at the cost of complexity. GraphQL is complex because:

  1. Learning Curve: GraphQL is a significant shift from RESTful API design, and learning it involves understanding new concepts like schema, resolvers, mutations, subscriptions, and more.
  2. Verbose: Writing a query or mutation in GraphQL can be verbose, even for simple operations. The specificity of the queries and mutations might seem unnecessary for simple data needs, as compared to a straightforward GET or POST request in REST.
  3. Increased Server Complexity: While GraphQL can move complexity from the client to the server, the server-side complexity increases. Implementing a GraphQL API involves defining a schema, setting up resolvers, handling nested queries, and more.
  4. Query Optimization and Performance: GraphQL’s flexibility can be a double-edged sword. In the absence of careful design, complex nested queries can potentially lead to performance issues. Unlike REST, where the server defines what data is returned, the onus of performance optimization is more on the server in GraphQL as the client dictates what data it needs.

Because REST APIs rely on in-built HTTP methods, effectively the complexity of networking is already abstracted away for you. This means developers can focus on defining the endpoints and data structures, without having to worry about the underlying communication protocol. These HTTP methods (the GET, POST, PUT, PATCH, DELETE above) provide a simple and intuitive way to manage data on the server and map naturally to database operations (create, read, update, delete), which further reduces complexity.

Moreover, the status codes provided by HTTP offer a ready-made, standardized way of communicating the result of a request — success, failure, or something in between — without having to design a separate system for this. In GraphQL, every response returns a 200 OK status code, and errors are handled within the response body. Developers must manually parse the response to check if any errors were encountered during data retrieval or mutation.

Finally, the stateless nature of REST, where each request is independent and does not require knowledge of previous requests, simplifies the server design and scalability. There’s no need to maintain, track, or manage sessions on the server side; each request contains all the information needed to process it.

So when to use which? Use GraphQL when:

  1. You need efficient data loading: If your application has complex requirements and needs to work with interconnected entities, GraphQL can be more efficient than REST. Because GraphQL allows you to query for exactly what you need, you can avoid over-fetching or under-fetching of data, which can improve the performance of your app.
  2. Your data requirements are complex: If your application has a lot of hierarchical data or many relationships between entities, GraphQL’s graph-based model can be easier and more intuitive to work with than REST’s resource-based model. This makes it a good choice for applications that need to handle complex queries involving multiple resources.
  3. Rapid product iterations on the frontend: If the frontend team wants to iterate quickly and needs to change the data structure frequently, GraphQL is a better fit as it provides the ability to request a custom set of data, reducing the need for backend changes every time a data modification is required.

For everything else, there’s REST. Use REST when:

  1. Simplicity is a priority: REST is conceptually simpler than GraphQL and relies on standard HTTP methods. This can make REST a better choice for simpler applications, or when development speed and understanding are critical.
  2. Built-in caching is important: HTTP caching is an efficient way to speed up your application, and REST APIs get this for free. If your application does a lot of reads and could benefit significantly from caching, REST might be a better choice.
  3. Bandwidth is a major consideration: GraphQL requires more bytes over the wire due to the length of the queries and responses. If bandwidth usage is a major consideration (for example, for mobile users with limited data plans), REST might be a better choice.
  4. Your application is largely CRUD-based: If your application primarily deals with CRUD (Create, Read, Update, Delete) operations and doesn’t need the flexibility of GraphQL queries, REST might be easier and more straightforward to use.

The power of simplicity

It may seem that the flexibility of GraphQL makes it much more powerful than REST. But simplicity brings its own power. Using REST allows you to build APIs quickly and simply, meaning developers can spend more time on the important parts of your application, like the business logic.

At Vessel, we use REST APIs for this purpose. We’re designing for developer experience–we want to make building integrations as simple as possible. So not only are we building a single, universal API for all of your do-to-market tools, but we also only use REST APIs to keep even this simple integration simple (we even simplify it further, and only use POST methods for our endpoints. No GETs, PATCHes, PUTs, or DELETEs).

Simplicity allows your engineering team to focus on creating the most valuable features, improving product quality, and innovating. By utilizing the power of REST’s simplicity, you expedite the process, streamline workflows, and ultimately, bring greater value to your users.

--

--

Vessel

A developer-first, native integration platform for GTM tools. We make it easy for developers to build deep integrations with their customers sales tools