Codementor Events

GraphQL vs REST - The Battle of the Servers

Published May 18, 2022
GraphQL vs REST - The Battle of the Servers

Hello Codementor!
In this post, we will code together both a standard REST server using expressjs as well as a GraphQL server using apollo-server. We will see the differences and benefits of both and hopefully learn some cool things in the process.

What is REST?

a REST API is "an architectural style for providing standards between computer systems on the web, making it easier for systems to communicate with each other" (Codecademy). The advantage of such a style is that any frontend client can connect to any backend server without one having intimate knowledge of the other. A REST API merely exposes endpoints to the world, which any client can ingest (with permission, of course, in the form of tokens / cookies). I invite you to read more in the article of the nitty gritty technical things, but I want to introduce the subject briefly and dive into the practicalities.
When calling a REST API, you can set any http headers, parameters and data in the body you wish, given the type of call and payload required by the API. The four main calls in what we call CRUD are

POST // This is used for creating a resource. C - Create
GET // This fetches a resource without mutating it. R - Read
PUT // This is standard for updating a resource. U - Update
DELETE // This deletes a resource. D - Delete

What is GraphQL?

GraphQL was born out of Facebook and addresses the issue of static endpoints. For example, if I want to fetch a list of users, but only want the name field of each user, I'd have to expose another endpoint such as /users/names, because /users would return me everything in that user and could be quite expensive. GraphQL is a service built on top of REST that allows you to fetch customisable payloads without having to build new endpoints, making your API concise and pretty.
I have developed a repository of two servers, one REST and one GraphQL, where the differences can be highlighted.. Please download and follow along with me!

In Action

Please follow the README to get started. I have set up a simple mock database using JSON files and have mimicked asynchronous functionality. There are two servers, one using ExpressJS and one using Apollo GraphQL. Let's explore the REST example first, found at src/servers/express/index.ts:

import express, { Request } from "express";
import {
  createPost,
  deletePost,
  deleteUser,
  getPosts,
  getUser,
  getUsers,
  login,
  signup,
  updatePost,
  updateUser,
} from "../../modules";
import { Post, PostUpdate, User, UserUpdate } from "../../types";
import { sendExpressError } from "../../utils/errors";
import {
  authorizePostFromReqParams,
  authorizeToken,
  authorizeUserFromReqParams,
} from "./auth";

const app = express();
app.use(express.json());
const port = 3000;

app.get("/users", async (_, res) => {
  try {
    const users = await getUsers();
    res.send(users);
  } catch (e: any) {
    sendExpressError(res, e);
  }
});

app.get("/users/current", authorizeToken, async (_, res) => {
  try {
    const user = await getUser(res.locals.userId);
    res.send(user);
  } catch (e: any) {
    sendExpressError(res, e);
  }
});

app.get(
  "/users/:id",
  authorizeToken,
  authorizeUserFromReqParams,
  async (req: Request<{ id: string }>, res) => {
    try {
      const user = await getUser(req.params.id);
      res.send(user);
    } catch (e: any) {
      sendExpressError(res, e);
    }
  }
);

app.put(
  "/users/:id",
  authorizeToken,
  authorizeUserFromReqParams,
  async (req: Request<{ id: string }, any, UserUpdate>, res) => {
    try {
      const response = await updateUser(req.body);
      res.send(response);
    } catch (e: any) {
      sendExpressError(res, e);
    }
  }
);

app.delete(
  "/users/:id",
  authorizeToken,
  authorizeUserFromReqParams,
  async (req: Request<{ id: string }>, res) => {
    try {
      const user = await deleteUser(req.params.id);
      res.send(user);
    } catch (e: any) {
      sendExpressError(res, e);
    }
  }
);

app.post("/signup", async (req: Request<any, any, User>, res) => {
  try {
    const token = await signup(req.body);
    res.send(token);
  } catch (e) {
    sendExpressError(res, e);
  }
});

app.get("/login", async (req: Request<any, any, any, Partial<User>>, res) => {
  try {
    const token = await login(req.query);
    res.send(token);
  } catch (e) {
    sendExpressError(res, e);
  }
});

app.get("/posts", async (_, res) => {
  try {
    const posts = await getPosts();
    res.send(posts);
  } catch (e: any) {
    sendExpressError(res, e);
  }
});

app.post(
  "/posts",
  authorizeToken,
  async (req: Request<any, any, Post>, res) => {
    try {
      const posts = await createPost({
        ...req.body,
        userId: res.locals.userId,
      });
      res.send(posts);
    } catch (e: any) {
      sendExpressError(res, e);
    }
  }
);

app.get("/users/current/posts", authorizeToken, async (_, res) => {
  try {
    const posts = await getPosts(res.locals.userId);
    res.send(posts);
  } catch (e: any) {
    sendExpressError(res, e);
  }
});

app.put(
  "/posts/:id",
  authorizeToken,
  authorizePostFromReqParams,
  async (req: Request<{ id: string }, any, PostUpdate>, res) => {
    try {
      const post = await updatePost({ ...req.body, id: res.locals.postId });
      res.send(post);
    } catch (e: any) {
      sendExpressError(res, e);
    }
  }
);

app.delete(
  "/posts/:id",
  authorizeToken,
  authorizePostFromReqParams,
  async (_, res) => {
    try {
      const result = await deletePost(res.locals.postId);
      res.send(result);
    } catch (e: any) {
      sendExpressError(res, e);
    }
  }
);

app.listen(port, () => {
  console.log(`EXPRESS SERVER RUNNING ON http://127.0.0.1:${port}`);
});

First, I import all my libs that interact with the data. Then, I declare app as an instance of express. Then, I declare my endpoints using get, post, put and delete accordingly.

I can then set up as much middleware as I wish, in the form of functions such as authorizeToken and authorizePostFromReqParams, found at src/servers/express/auth.ts:

const authorizeToken = (req: Request, res: Response, next: NextFunction) => {
  try {
    const token = (req.headers.authorization || "")
      .replace("Bearer", "")
      .trim();
    if (!token) {
      throw new Error("No Authentication Token Provided");
    }
    const decoded = verify(token, "JWTSECRET!");
    res.locals.userId = decoded;
    next();
  } catch (e) {
    sendExpressError(res, e, 403);
  }
};

const authorizePostFromReqParams = async (
  req: Request<{ id: string }>,
  res: Response,
  next: NextFunction
) => {
  try {
    const { id: postId } = req.params;
    const canEdit = (await getPost(req.params.id)).userId === res.locals.userId;
    handleError(!canEdit, "You are unauthorized to edit this post.");
    res.locals.postId = postId;
    next();
  } catch (e) {
    sendExpressError(res, e, 403);
  }
};

I can then update my context using res.locals for the next piece of middleware. The middleware is used to determine authorization based on existence of a JSON web token, or ability to edit / delete posts / users if the current user is the author of that post.

Let's give it a go! Spin up the server following the README instructions, open Postman, a free API endpoint tester, and sign up a user:
Screenshot 2022-05-18 at 08.10.13.png
You can see that a token is returned! Also, now if you check the file src/data/users.json, you will see the new entry, as if it were in a database!
Screenshot 2022-05-18 at 08.13.07.png

Setbacks

Add your token into the authorization:
Screenshot 2022-05-18 at 08.41.31.png
and let's fetch the user:
Screenshot 2022-05-18 at 08.42.17.png
So this successfully fetches the user from the database. But can you see I'm also fetching the password field? That field will never have any use for me. I can possibly modify the method to remove the password field when the user is returned, but then I would have a lot more bloat in my code for something seemingly simple to do.
Another setback is such:
Screenshot 2022-05-18 at 08.45.15.png
And you can see it successfully saved to the database:
Screenshot 2022-05-18 at 08.47.03.png
This is a problem. REST APIs have no built-in validation. Yes, I know you will need to have a validation library from the backend side (Joi is my favourite!) but out the box, there's a lot more configuration required for a REST API.

Enter GraphQL

Let's explore how the GraphQL server is set up, in src/servers/graphql/index.ts:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  context: ({ req }) => {
    if (req.headers.authorization) {
      return { id: verify(req.headers.authorization, "JWTSECRET!") };
    }
  },
});

// The `listen` method launches a web server.
server.listen().then(({ url }) => {
  console.log(`🚀  APOLLO GRAPHQL SERVER RUNNING AT ${url}`);
});

Every graphql server needs typeDefs and resolvers. TypeDefs are the schema of your graphql API, and you can see that it is a lot tighter and more defined than normal REST. There are five primitive types: String, Int, Float, Boolean and ID. You can then define schema using those types. There are also Third Part Scalar Types you can use in a project
There are three main methods: Queries, Mutations and Subscriptions. Queries are like your GET methods in REST. Mutations, as named, are responsible for mutating data and are like POST and PUT methods. Subscriptions are responsible for real-time events. You can subscribe to a certain topic and whenever there's a change there, and event will be fired. I will not go into subscriptions in this article. See my typeDefs below (src/servers/graphql/typedefs.ts):

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

  type Post {
    id: ID!
    userId: ID!
    user: User!
    title: String!
    subtitle: String
    body: String
  }

  ## These are my Input Schema for Mutations

  input UserCreateInput {
    id: ID!
    name: String!
    password: String!
  }

  input UserUpdateInput {
    id: ID!
    name: String
  }

  input PostCreateInput {
    title: String!
    subtitle: String
    body: String
  }

  input PostUpdateInput {
    id: ID!
    title: String
    subtitle: String
    body: String
  }

  ## These are my Mutations (equivalent to POST / PUT)

  type Mutation {
    signup(input: UserCreateInput!): String! #returns a token
    createPost(input: PostCreateInput!): Post!
    deleteUser: Boolean!
    deletePost(id: ID!): Boolean!
    updateUser(input: UserUpdateInput!): User!
    updatePost(input: PostUpdateInput!): Post!
  }

  ## These are my Queries (equivalent to GET)

  type Query {
    login(id: ID!, password: String!): String!
    getPost(id: ID!): Post!
    getPosts: [Post]!
    getUser(id: ID): User!
    getUsers: [User]!
  }

Each query and mutation is defined by what variables are required or not. an exclamation mark means the variable is required, or the return value is required. For example, login requires an id and password and MUST return a string

Resolvers

The power of graphql comes in from the resolvers (src/servers/graphql/resolvers.ts). A resolver is a Javascript object that instructs the graphql server what to do when each endpoint is hit:

  createPost(
      _: any,
      { input }: { input: PostCreate },
      context?: GQLUserContext
    ) {
      authorizeToken(context);
      return createPost({ ...input, userId: context!.id });
    },

I am taking one endpoint as an example. createPost here is a function. Every graphql resolver has four arguments you are allowed to use. The first is parent, which we'll explain later. The second is args, ie the variables passed though from the schema. In this example, the args would be the object PostCreateInput and the fields defined there. The context variable is everything I have defined in the context on my server:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true,
  context: ({ req }) => {
    if (req.headers.authorization) {
      return { id: verify(req.headers.authorization, "JWTSECRET!") };
    }
  },
});

In this case, it would be an object with the user's id / email address if a token is present, or null if absent.
The last argument is info, which is only used in advanced cases and describes the execution of the call.
So for the example above, createPost, first I am authorizing the Token (do I have a token?) and if so, I then return the method createPost from my lib, using the userId as the id from my context, since my logic is I need to be logged in to create a post.

Let's open up the graphql endpoint and see the power. run npm run dev-graphql and open http://localhost:4000/graphql First off, every graphql server has a built-in playground that loads your schema, gives fantastic intellisense and allows you to hit the endpoints:
Screenshot 2022-05-18 at 08.56.46.png
Also, it screams at me if I try add in any field that isn't part of my schema:
Screenshot 2022-05-18 at 08.57.57.png
Which means my API is protected from bad data on the get-go! Obviously, further validation such as checking if it's a good email and other checks, would need a validation library, but this is a good start!
Let's sign up, and put the token you receive in the Authorization header:
Screenshot 2022-05-18 at 09.16.25.png
and let's create a Post:

mutation{
  createPost(input: {title:"A POST+!"}) {
    id
    user {
      id
      name
    }
  }
}

the method createPost returns a Post object, but what's this

user {
  id
  name
}

going on here? And look at the result:
Screenshot 2022-05-18 at 09.18.49.png

The Power of Resolvers

If you look in your resolvers, you can see that not only am I resolving Query and Mutation types, I can also resolve custom schema!

  Post: {
    user: (post: Post) => getUser(post.userId),
  }

I am using the parent arg here, which is the Post object, and I am saying that to resolve the user field I defined in my Post type in the graphql, I want to call the getUser method.
The inverse is even more powerful:

  User: {
    posts: (user: User) => getPosts(user.id),
  }

Here, the parent is the User object, and I want to resolve all the user's posts in the same schema, so if I call:

query{
  getUser {
    id
    name
    posts {
      id
      title
      body
    }
  }
}

My result set resolves to fetch all the user's posts. This is much more compact than having to GET the user object and then GET the posts array in a normal REST API. I could have the posts array in the user GET request, but what if I don't want the posts for this specific call? Graphql is dynamic enough to allow me not to fetch the posts if I don't actually need them. Just leave them out of your Query, and no resolving will be done!

query{
  getUser {
    id
    name
  }
}

Setbacks of GraphQL

A standard REST api has a lot more customization out of the box for status codes that are returned. a GraphQL endpoint is essentially a POST call to the graphql server:
Screenshot 2022-05-18 at 09.30.32.png
Even if there is an error in the method, GraphQL will return a 200 with an Error object, which can be translated on the client side though:
Screenshot 2022-05-18 at 09.31.56.png

Which is better?

It depends. I for one prefer graphql when it comes to basic JSON packets from server to client, however passing through Blobs / files would require some sort of REST api. Thankfully, You can combine both REST and GraphQL Apis! One endpoint of your REST api can be an entire graphql server, and you can leverage other endpoints for requests more suited to REST.

Conclusion

We built and explored a repository where both REST and GraphQL servers were set up to manipulate the same data. We explored the advantages and pitfalls of both and I hope this served as a nice springboard to the world of APIs.

Happy coding!

~ Sean

Discover and read more posts from Sean Hurwitz
get started
post commentsBe the first to share your opinion
Show more replies