Bryan Rodriguez, Lead Software Engineer at EPAM Anywhere, takes you through the process of exposing GraphQL API using Apollo Server and wraps it up with the pros and cons of this approach.
GraphQL is an open-source query language and a server runtime for APIs. It provides a way to consume web services through its own SDL Specification and Definition Language, which lets it execute queries and mutations to fetch data from the server.
Let’s focus on some basic concepts to make sure that you understand how GraphQL works.
With GraphQL, you have three ways to consume services: Queries, Mutations, and Subscriptions.
Queries are for fetching data (think of it as the GET HTTP verb in Rest APIs).
Mutations are for changing data (similar to the POST, PUT, DELETE HTTP verbs in Rest APIs):
Subscriptions let you update data more frequently using a publish and subscribe model with the help of the WebSocket protocol. Note, however, that this might not be suitable for every use case, so you really need to know when and where to apply it.
GraphQL services are exposed through a single endpoint, generally /graphql, but this can be changed (which is recommended). This endpoint is used to receive the requests to the server, using the GraphQL SDL language specification for the Queries and Mutations sent in the body through POST requests.
Exposing GraphQL API with Apollo Server
It’s important that, in the backend implementation, you can use Apollo Server not only as the traditional client-server model but also as a gateway for a federated graph (think of it as an aggregation of other graph services). In addition, you can use it for serverless lambda functions and add it to your existing Node.js application when you are using Express, Fastify, Koa, or another similar framework.
Below, I show a basic example of a Node.js server and expose the GraphQL API with Apollo Server. Let’s begin.
Step 1: Setup
In your brand-new npm project folder (already initialized with $ npm --init), install the required dependencies for GraphQL and Apollo Server using $ npm install graphql apollo-server.
After that, create your entry point file (it can be index.js or app.js) and add the next line:
Step 2: The schema model
Also known as type definitions, the GraphQL schema is the most important part of the GraphQL API because it will provide the entities available to be used in our queries and mutations.
In the next example, I will use the type User as the entity available in any of our queries or mutations, including it into the type definitions:
In addition to the type, we need to include the Query type, which is the special root type which will let us fetch data and create queries of our schema.
Inside it, we can include all the queries we want to export in the GraphQL schema by defining the name, the params required, and the type returned (similar in some way to a typescript function) for each query.
In this case, I'm including the users query which returns a collection (array) of users. Additionally, there is the getUser query which receives, as a mandatory param, the username which is the type String, and the query retrieves a single User.
Error debugging tip: If you don’t include the Query root type, you will see Error: Query root type must be provided, as follows:
There are other root types which should also be included. The Mutation type is one example:
Step 3: Resolvers
The resolvers work similarly to the controllers in Rest APIs — they oversee handling the requests to certain mutations or queries and provide the response.
This means that for each mutation, there should be a resolver, and for each query both mutation and resolver should be declared in the schema definition. As a rule, they need to be named in the same way as they were defined in the schema of the type definitions above.
Here’s an example:
In this example, I’m using as a data source a memory-stored variable with a user’s array following the schema guidelines:
So, in the users query, the resolver function basically returns the whole users stored. In the getUser query, the resolver function is extended to include all the params accepted, but here I’m using only the args param defining the arguments passed to the query.
The createUser mutation receives the username and email as arguments, and adds the new one to the users array unless an unexpected error occurs, in which case the exception will be thrown with the message “User could not be created.”
Step 4: Finishing the server setup
To finish the server setup, just instantiate the server and listen to a specified port:
Now you can run it from your terminal using $ node app.js.
Finally, you are ready to execute queries and mutations into the new server. To do so, you can simply use the Apollo GraphQL interface. Go to http://127.0.0.1:4000/graphql, and click on the Query your graph button. You can use a GUI not only to execute queries and mutations, but also to explore the Schema types, mutations, queries, and more.
Step 5: Extending the schema and adding features
There are ways to extend our schema.
For example, what if my current users are employees, and I want to include that as a new entity?
First, I will include the Employee type, then include it as a nested entity into the User type in my schema as follows:
It’s important to check the implications this will have in the resolvers and data sources, and implement these changes if needed.
In our case, I could try to fetch the employee nested entity inside the User of the getUser query.
The result is that the obtained value for the employee atribute is null as shown below.
Why didn’t it trigger an error, you might ask? In the schema, we are not specifying that employee is a required attribute for each user (if we need it, we can use the ! Operator).
Why is employee null? Essentially, it’s because of the absence of that data in our current data source, which in our case is the users variable with mocked memory-stored data. To fix this, we could include the data in our mocked data source as follows:
Then execute the query again:
Why did it respond with an error? The type validation made by GraphQL is telling us that the value set in the salary of the employees doesn’t match the one described in the schema, which is Float, and is instead retrieving “100k year” which is a String.
We can fix this by simply using the corresponding type, changing it to:
And running the query again:
This is not necessarily the best way to proceed, because the salary as shown doesn’t express a monetary value, because the currency is missing and the salary may include periodicity.
Let’s add it to the Schema:
Then, add it to our mocked data source:
And run the updated query:
Now the response is more generic and semantic in the same way.
With that, our GraphQL schema corresponds to the following diagram:
The pros and cons of GraphQL with Apollo
The example above demonstrates the powerful advantages and the versatility of GraphQL to expose services, but it represents only a small part of the available functionality.
In conclusion, here are some of the pros and cons of GraphQL based on my experience:
- Schema consistency. GraphQL is focused on providing a schema as a contract that lets us mimic the database schema, but focus only on the information we are interested in.
- Type checking. This is especially useful when you are using dynamic typed languages and want to reduce validations from the web services side.
- Avoids the over-fetching problem. You will only fetch what you want to fetch, so if you need only a few of the attributes of an entity, you can access only those attributes.
- Better communication between backend and frontend developers using the schema definition could enhance development processes. This is true because the schema definition is a contract and, without any backend implementations, the frontend team can start working with mocked data based on the schema.
- No need to create multiple services and ability to cover more use cases in a more generic and semantic way.
- Code-first or schema-first approaches are both available. The one used in this piece is schema-first.
- Multiple ways to modularize the code. Essentially, the resolvers are like the C of the MVC pattern. If you are using the schema-first approach, you could use tools like graphql-tools to split the schema into queries, mutations, types, input types, and so on, depending on what you need to do.
- Some code duplication. This happens mostly when using the schema-first approach, but if you decide to use the code-first approach, I recommend one that uses decorators, which is more declarative and definitively reduces a lot of code.
- Difficulty of debugging schema issues. Some basic types of errors are very easy to catch, but some of the schema (human) errors are a bit more difficult (if you are using the schema-first approach). I recommend paying extra attention to error handling using exceptions, too. Apollo offers a good way to extend their own exceptions.
- Scalars should be included manually. This is a little tricky, but ultimately a powerful feature because, for languages with no types like Long, you can create or define your own scalars based on your business use cases.
- There is a single endpoint over HTTP to handle all the requests (this isn’t necessarily a bad thing, though).
- The N+1 problem. This can be successfully solved using data loaders, which can be challenging to use but, once implemented, are very powerful.
That’s it! Hope you found this Apollo GraphQL tutorial valuable. Stay tuned for more guides and tips from our software engineers here at EPAM Anywhere. If you’re considering becoming one, feel free to explore our remote software engineer jobs and apply!