javascript

Building APIs With GraphQL in Your Node.js Application

Diogo Souza

Diogo Souza on

Last updated:

Building APIs With GraphQL in Your Node.js Application

This post was updated on 14 August 2023 to include changes in Apollo Server 4 and express-jwt v8.

REST has reigned for a long time in the world of web services. It's easy to implement, allows standardization through RESTful patterns, and has lots of libraries that support and facilitate its development. Then came GraphQL, the famous query language for APIs.

Now that you're diving into GraphQL and Node.js, this might be the time to learn about monitoring GraphQL and Node.js too.

What’s GraphQL

To better understand GraphQL, we need to look at what defines it. GraphQL was created to be:

  • declarative — meaning, you should have the power to choose the data that you want. In other words, you query (request for) some data, defining exactly what you want to get (that is where the schema comes in).
  • compositional — just like it is in many programming language objects, you can have one field inheriting from another or inside another. Or from both, if you prefer.
  • strongly-typed — once a field has its type defined, that’s it—a different type isn't allowed.
  • self-documented — the schema, by itself, offers great documentation (with data types, structure, queries, mutations, etc.).
  • less verbose — we only get what we asked, which greatly differs from REST, which gives you everything (which isn't very efficient, especially if this everything means a lot of unnecessary data).
  • among others.

GraphQL is a whole new paradigm. It brings to light the discussion of whether your APIs should have organized and well-structured request and response data in the same way we have when programming data structures in our back-end applications.

The more the number of points discussed above that your API lacks, the more of an indicator that it could benefit from GraphQL. But you don’t have to abruptly migrate to it. Some developers start slowly by creating and exposing some endpoints and asking the clients to consume them. In that way, they gather more insight from both sides that determine if that’s the right path to take.

In this article, we'll create a GraphQL HTTP server with Express and Apollo Server.

Let’s go to some practical stuff. Nothing better than seeing in action how GraphQL fits into a common API example. For this, we’ll be creating a complete API to access some beer data.

First, our API example will enable the registration, login, and authentication of users. This way, we can ensure it's secure and unauthorized users can't see our favorite beer list.

Then, we’ll dive into the construction of our API operations, set up a Postgres database to store the credentials and tokens, as well as test everything out.

After we finish, we can celebrate with a beer from our list. So let’s get started.

👋 Did you know that AppSignal APM for Node.js has automatic instrumentation for Apollo? And all the slow API requests automatically show up on the Slow API screen.

Setting Up Our Project

The example we’re about to develop expects that you have Node.js installed. We recommend the latest LTS version.

Next, select a folder of your preference and run the following commands:

shell
# Set up a new project $ npm init -y # Install dependencies $ npm install @apollo/server graphql bcrypt express express-jwt jsonwebtoken pg pg-hstore sequelize cors body-parser # Install dev dependency $ npm install --save-dev sequelize-cli

They initialize our Node project with default settings, install the npm dependencies required for the GraphQL + Apollo example, and install the Sequelize CLI Tool, respectively.

Regarding the dependencies, we have:

  • @apollo/server is the main library for Apollo Server. It knows how to turn HTTP requests and responses into GraphQL operations and run them.
  • graphql: the implementation per se of GraphQL in JavaScript.
  • bcrypt: it’ll be used to hash our passwords.
  • express and express-jwt: the Express framework itself along with the middleware for validating JWT (JSON Web Tokens) via the jsonwebtoken module. There are a bunch of ways of dealing with the authentication process, but in this article, we’ll make use of JWT bearer tokens.
  • pg and pg-hstore: the client for Postgres and the serializer/deserializer of JSON to hstore format (and vice versa).
  • sequelize: the Node.js ORM for Postgres (among other databases) that we’ll use to facilitate the job of communicating with the database.
  • cors and body-parser: will be used to set up HTTP body parsing and CORS headers for our server.

Next, we'll use the Sequelize CLI tool to initialize our Node project as an ORM one:

shell
$ npx sequelize-cli init

That will create the following folders:

  • config - contains a config file, which tells the CLI how to connect with the database
  • models - contains all models for the project
  • migrations - contains all migration files
  • seeders - contains all seed files

Now, let’s move on to the database related configs. First of all, we need a real Postgres database. If you still don’t have Postgres installed, then go ahead. As a GUI tool for managing the database, we’ll use pgAdmin. We'll use the web GUI that comes with it.

Next, we’ll create our example’s database. For this, access the web pgAdmin window and create it:

Creating the database

Then, go back to the project and update the content of config/config.json as shown:

json
"development": { "username": "postgres", "password": "postgres", "database": "appsignal_graphql_db", "host": "127.0.0.1", "dialect": "postgres" },

We’re only showing the development section since it’s the only one we’ll be dealing with in the article. However, make sure to update the other related ones as well before deploying your app to production.

Next, let’s run the following command:

shell
npx sequelize-cli model:generate --name User --attributes login:string,password:string

This is another command from Sequelize framework that creates a new model in the project—the user model, to be exact. This model will be important to our authentication structure. Go ahead and take a look at what's been generated in the project.

For now, we’ll only create two fields: login and password. But feel free to add any other fields you judge important to your design.

You may also notice a new file created under the migrations folder. There, we have the code for the user’s table creation. In order to migrate the changes to the physical database, let’s run:

shell
npx sequelize-cli db:migrate

Now you can check the results in pgAdmin:

List of created tables

You may wonder where's the table that will store our beer data. We won’t store it in the database. The reason is that I’d like to demonstrate both paths: fetching from the db and from a static list in the JavaScript code.

The project’s set. Now we can move on to implementing the authentication.

Let’s Authenticate!

The authentication must be implemented first because no other API method should be exposed without proper safety.

Let’s start with the schema. The GraphQL schema is the recipe that the API clients must follow to properly use the API. It provides the exact hierarchy of field types, queries, and mutations that your GraphQL API is able to execute. It is the contract of this client-server deal. With very strong and clear clauses, by the way.

Our schema should be placed in the schema.js file. So, create it at the root of the project and add the following content:

javascript
const typeDefs = `#graphql type User { id: Int! login: String! } type Beer { id: Int! name: String! brand: String price: Float } type Query { current: User beer(id: Int!): Beer beers(brand: String!): [Beer] } type Mutation { register(login: String!, password: String!): String login(login: String!, password: String!): String } `; module.exports = typeDefs;

For more details on how the schema is structured, please refer to this. In short, the Query type is where we place the API methods that only return data, and the Mutation type is where the methods that create or change data go.

The other types are our own types, like Beer and User—the ones we create to reflect the JavaScript model that will be defined in the resolvers.

The #graphql tag is used to infer syntax highlighting to your editor plugin (like Prettier). It helps to keep the code organized.

The resolvers, in turn, are the executors of the methods defined in the schema. While the schema worries about the fields, types, and results of our API, the resolver takes all this as reference and implements the execution.

Create a new file called resolvers.js at the root and add the following:

javascript
const { User } = require("./models"); const bcrypt = require("bcrypt"); const jsonwebtoken = require("jsonwebtoken"); const JWT_SECRET = require("./constants"); const resolvers = { Query: { async current(_, args, { user }) { if (user) { return await User.findOne({ where: { id: user.id } }); } throw new Error("Sorry, you're not an authenticated user!"); }, }, Mutation: { async register(_, { login, password }) { const user = await User.create({ login, password: await bcrypt.hash(password, 10), }); return jsonwebtoken.sign({ id: user.id, login: user.login }, JWT_SECRET, { expiresIn: "3m", }); }, async login(_, { login, password }) { const user = await User.findOne({ where: { login } }); if (!user) { throw new Error( "This user doesn't exist. Please, make sure to type the right login." ); } const valid = await bcrypt.compare(password, user.password); if (!valid) { throw new Error("You password is incorrect!"); } return jsonwebtoken.sign({ id: user.id, login: user.login }, JWT_SECRET, { expiresIn: "1d", }); }, }, }; module.exports = resolvers;

The resolvers follow a pattern that’s inherently async because it’s Promise-based. Each operation must have the exact same signature as the one defined in the schema.

Note that, for all query operations, we’re receiving a third argument: user. That one is going to be injected via context (still to be configured in index.js).

The jsonwebtoken dependency now takes over signing in the user according to the provided credentials and then generating a proper JWT token. This action will happen in both registration and login processes.

Also, notice that an expiry time must be set for the token.

Finally, there’s a JWT_SECRET constant that we’re using as the value for secretOrPrivateKey. That is the same secret we’ll use in the Express JWT middleware to check if the token is valid.

This constant will be placed in a new file, called constants.js. Here’s its content:

javascript
const JWT_SECRET = "sdlkfoish23@#$dfdsknj23SD"; module.exports = JWT_SECRET;

Make sure to change the value to a safe secret of yours. The only requirement is that it be long.

Now, it’s time to configure our index.js file. Create the file at the root of the project and add the following to it:

javascript
const { ApolloServer } = require("@apollo/server"); const { expressMiddleware } = require("@apollo/server/express4"); const { ApolloServerPluginDrainHttpServer, } = require("@apollo/server/plugin/drainHttpServer"); const cors = require("cors"); const bodyParser = require("body-parser"); const express = require("express"); const http = require("http"); const { expressjwt: jwt } = require("express-jwt"); const typeDefs = require("./schema.js"); const resolvers = require("./resolvers.js"); const JWT_SECRET = require("./constants.js"); const app = express(); const httpServer = http.createServer(app); const auth = jwt({ secret: JWT_SECRET, algorithms: ["HS256"], credentialsRequired: false, }); app.use(auth); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); server.start().then(() => { app.use( "/graphql", cors(), bodyParser.json(), expressMiddleware(server, { context: async ({ req }) => { const user = req.auth ? req.auth : null; return { user }; }, }) ); httpServer.listen({ port: 3000 }, () => { console.log(`Server ready at http://localhost:3000/`); }); });

If you use Express as your web server, this code may look familiar.

Express app is going to be used as usual. We’re creating it, adding a middleware (jwt), and starting it up. However, the ApolloServer may come along to add the necessary GraphQL settings.

ApolloServer receives the schema (typeDefs), resolvers, and an optional ApolloServerPluginDrainHttpServer plugin as arguments. The ApolloServerPluginDrainHttpServer plugin is recommended for use with expressMiddleware to ensure that your server shuts down gracefully.

The expressMiddleware function enables you to attach Apollo Server to an Express server. To use it, you have to set up HTTP body parsing and CORS headers for the server.

expressMiddleware accepts two arguments. The first is an instance of ApolloServer that has been started by calling its start method and the second is an optional context.

The context, is an optional attribute that allows us to make quick conversions or validations prior to the GraphQL query/mutation executions. In our case, we’ll use it to extract the auth object from the request and make it available to our resolvers functions.

This is it. Let’s test it now. Run the application with the following command:

shell
node index.js

Then, access the address http://localhost:3000/graphql and the Apollo Sandbox view will show up.

Our first test will be to register a new valid user. So, paste the following snippet into the Operation area and hit the Run button:

javascript
mutation { register(login: "john", password: "john") }

A valid token will return as shown in the figure below:

Registering a new user

This token can already be used to access sensitive methods, like the current.

If you don’t provide a valid token as an HTTP header, the following error message will be prompted:

Error of not authenticated user

To send it properly, click the Headers tab at the bottom of the page and add a new header with an Authorization key and Bearer [token] value:

plaintext
header key = Authorization value = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NSwibG9naW4iOiJhcHBzaWduYWwiLCJpYXQiOjE1ODk5MTYyNTAsImV4cCI6MTU4OTkxNjQzMH0.bGDmyi3fmEaGf3FNuVBGY7ReqbK-LjD2GmhYCc8Ydts

Make sure to change the content after Bearer to your version of the returned token. You will have a result similar to the figure below:

Querying for the current user

Obviously, if you already have a registered user, you can get the token by logging in via login mutation:

javascript
mutation { login(login: "appsignal", password: "appsignal") }

Once again, if one of your credentials is wrong, you’ll get the corresponding error message.

Our Beer API

For the sake of simplicity, we won’t create our Beer domain in the database. A single JS file will do the job. But I’d recommend that you migrate to our ORM model as well, making use of the knowledge you’ve got so far.

Let’s start with this, then. This is the code for our beers.js file (make sure to create it too):

javascript
var beersData = [ { id: 1, name: "Milwaukee's Best Light", brand: "MillerCoors", price: 7.54, }, { id: 2, name: "Miller Genuine Draft", brand: "MillerCoors", price: 6.04, }, { id: 3, name: "Tecate", brand: "Heineken International", price: 3.19, }, ]; module.exports = beersData;

Feel free to add more data to it. I reserve the right of not knowing their correct prices.

Once the main GraphQL setup structure has been set, adding new operations is quite easy. We just need to update the schema with the new operations (which we’ve already done) and add the corresponding functions into the resolvers.js.

These are the new queries:

javascript
async beer(_, { id }, { user }) { if (user) { return beersData.filter((beer) => beer.id == id)[0]; } throw new Error("Sorry, you're not an authenticated user!"); }, async beers(_, { brand }, { user }) { if (user) { return beersData.filter((beer) => beer.brand == brand); } throw new Error("Sorry, you're not an authenticated user!"); },

They’re simply filtering the data based on the given arguments. Don’t forget to import the beersData array object:

javascript
const beersData = require("./beers");

Restart the server and refresh your Sandbox page. Note that we made those new queries safe too, so it means you’ll need to provide a valid token as header.

This is the result of a query by brand:

Querying with query variables

In this call, we’re making use of Query Variables. It allows you to call GraphQL queries by providing arguments dynamically. It’s very useful when you have other applications calling the GraphQL API, rather than just a single web IDE.

This is the magic of GraphQL. It allows even more complicated query compositions. Imagine, for example, that we need to query two specific beers in one single call, filtering by a list of ids.

Currently, we only have operations that filter by one single id or one single brand name. Not with a list of params.

Instead of going directly to the implementation of a new query function that would do it, GraphQL provides a feature called Fragments. Look how our query would be:

javascript
query getBeers($id1: Int!, $id2: Int!) { beer1: beer(id: $id1) { ...beerFields } beer2: beer(id: $id2) { ...beerFields } } fragment beerFields on Beer { id name brand price }

For this case, you’d need to provide the exact beer name for each of the results. The fragment defines from where it’s going to inherit the fields, in our case, from the Beer schema.

Basically, fragments allow you to build a collection of fields and then include them in your queries. Don’t forget to feed the Query Variables tab with the ids:

javascript
{ "id1": 1, "id2": 3 }

The result will look like the following:

Fragment's query example

Remember, you still need to include the Authorization header in the Headers tab.

Conclusion

It took a while, but we got to the end. Now you have a fully functional GraphQL API designed to provide queries and mutations and, more importantly, in a secure manner.

There is a lot you can add here. Migrate the Beer’s model to store and fetch data directly from Postgres, insert some logs to understand better what’s going on, and place some mutations over the main model.

Apollo + Express + GraphQL have proven to be a great fit for robust and fast web APIs. To learn more, please be sure to visit http://graphql.org/learn/. Great resource!

P.S. If you liked this post, subscribe to our new JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you want to have your GraphQL API monitored without any setup needed, try out AppSignal application monitoring for Node.js.

Diogo Souza

Diogo Souza

Diogo Souza has been passionate about clean code, software design and development for more than ten years. If he is not programming or writing about these things, you'll usually find him watching cartoons.

All articles by Diogo Souza

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps