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.
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 and 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.
When it comes to the Node.js universe, we have a bunch of useful tools to help out. express-graphql, for example, is one of the popular server middlewares for integrating GraphQL with Node.js. Apollo is a piece of cake in terms of GraphQL APIs development. It embraces some of the downsides of express-graphql, like the easy enabling of graphql-tools and its patterns. We’ll see more on this later.
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 launched the support for Node applications? 🎉 We'll be rolling out a whole array of integrations one by one in the coming weeks. Check out the docs and learn how to add your Node app to AppSignal.
Setting Up Our Project
The example we’re about to develop expects that you have Node.js installed. Make sure that it's at least version 8.0.
Next, select a folder of your preference and run the following commands:
npm init -y
npm i apollo-server-express bcrypt express express-jwt graphql jsonwebtoken pg pg-hstore sequelize
npm install -g 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-express: provides direct connection between Express and Apollo GraphQL server.
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.
Note that the Sequelize CLI tool had to be installed globally, otherwise, it wouldn’t be available at any command line interface. As its first command, let’s run the one that will initialize our Node project as an ORM one:
sequelize init
It will create some folders related to the ORM framework, like models
, config
and migrations
(since the framework also handles the migration of our databases).
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",
"operatorsAliases": false
},
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:
sequelize 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:
sequelize 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 and add the following content:
const { gql } = require("apollo-server-express");
const typeDefs = gql`
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 gql
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 behind.
Create a new file called resolvers.js
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. Substitute its content with the following:
const express = require("express");
const { ApolloServer } = require("apollo-server-express");
const jwt = require("express-jwt");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const JWT_SECRET = require("./constants");
const app = express();
const auth = jwt({
secret: JWT_SECRET,
credentialsRequired: false,
});
app.use(auth);
const server = new ApolloServer({
typeDefs,
resolvers,
playground: {
endpoint: "/graphql",
},
context: ({ req }) => {
const user = req.headers.user
? JSON.parse(req.headers.user)
: req.user
? req.user
: null;
return { user };
},
});
server.applyMiddleware({ app });
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log("The server started on port " + PORT);
});
If you use Express as your web server, this code may look familiar, except for the fact that we have two servers being set here.
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
, playground
and a context
as arguments. The playground
property states which endpoint is going to redirect to Prisma’s GraphQL Playground view. It is a built-in IDE to help us with testing of our GraphQL APIs.
The context
, in turn, 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 user
object from the request and make it available to our resolvers functions.
The server
object is the one that applies the middleware, passing the app
object as a param.
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 Playground view will show up.
Our first test will be to register a new valid user. So, paste the following snippet into the query area and hit the “Execute Query” 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 “HTTP HEADERS” tab at the bottom of the page and add the following:
{
"Authorization": "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 Playground 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:
Notice that the Authorization header is also there, hidden in the 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'd love an all-in-one APM for Node or you're already familiar with AppSignal, go and check out AppSignal for Node.js.
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.
Top comments (0)