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:
# 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:
$ npx sequelize-cli init
That will create the following folders:
config
- contains a config file, which tells the CLI how to connect with the databasemodels
- contains all models for the projectmigrations
- contains all migration filesseeders
- 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:
Then, go back to the project and update the content of config/config.json
as shown:
"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:
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:
npx sequelize-cli db:migrate
Now you can check the results in pgAdmin:
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:
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:
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:
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:
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:
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:
mutation { register(login: "john", password: "john") }
A valid token will return as shown in the figure below:
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:
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:
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:
Obviously, if you already have a registered user, you can get the token by logging in via login
mutation:
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):
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:
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:
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:
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:
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:
{ "id1": 1, "id2": 3 }
The result will look like the following:
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.