Creating advanced GraphQL API quickly using Spikenail

Igor Lesnenko
7 min readJul 31, 2017

--

This tutorial shows how to create GraphQL API for Trello-like application using Node.js and Spikenail framework. We will learn how to create models, define relations, setup access control and validations.

Complete source code which we are going to create in this tutorial is available here: https://github.com/spikenail/spikenail-example-cards

It is worth to notice that Spikenail framework is still on an early stage of development, your feedback for it will be greatly appreciated.

Application design

Our application will have Boards, Lists and Cards like Trello does. Every Board contains many Lists. Every List contains many Cards.

Authenticated users can create private and public boards. Public boards are visible to everyone including anonymous users. Private boards are only visible to a creator and users who get an access.

A user added to a board can have 2 different roles: either a member or an observer. Members and board owner can edit both private and public boards, observers can only read boards without editing it.

Brief introduction to GraphQL

GraphQL is a query language, created by Facebook. It provides an entirely new way for consuming and building the APIs. Basically, GraphQL is a replacement for REST.

One of the biggest advantages of GraphQL is an ability to query the exact resources we need and replace multiple API calls by a single request.

Take a look at an example request. We are querying only particular fields and a related model in a single query:

{
getBook(id: 123) {
title
author {
id
name
}
}
}

Example response:

{
"data": {
"getBook": {
"title": "The Great Gatsby",
"author" {
"id": 192501,
"name": "F. Scott Fitzgerald"
}
}
}
}

You can learn more about GraphQL on the official website.

Spikenail framework

Spikenail is an open-source Node.js ES7 framework which allows to build GraphQL API with little or no coding at all.

The core idea of Spikenail is allowing to create a complex API just by configuring it. This configuration might include relations, validations, access control and everything we need.

With this Spikenail provide enough flexibility by allowing to override every aspect of predefined behaviour.

Read more about Spikenail here: https://github.com/spikenail/spikenail

Prerequisites

You have node.js 7.6+ installed

You have MongoDB database

Installation

Install yeoman generator for Spikenail

npm install -g generator-spikenail

Create a directory for your project and run yeoman generator inside it

mkdir spikenail-example-cards
cd spikenail-example-cards
yo spikenail

It will scaffolds a basic directory structure and install all necessary dependencies.

Configure the data source

Currently, only MongoDB is supported. Set SPIKENAIL_EXAMPLE_CARDS_MONGO_CONNECTION_STRING environment variable or edit config/sources.js file.

Create models

Let’s start from creating a basic board model.

You can use the model generator in order to simplify a model creation:

yo spikenail:model board

This will create models/Board.js file with only id field

Let’s edit it and add some properties, so our Board.js become:

All boards that user creates are private by default. memberships is an array that will store sharing information in format:

[{
userId: 123
role: 'observer'
}, {
userId: 456,
role: 'member'
}]

memberships array should not be directly editable. So we use the readOnly property on it. As a result, it will not be exposed in update mutation parameters at all. Access sharing has more complex logic than just modifying field in the database — usually, it includes sending emails, an ability to invite unregistered users etc., we will not implement it in our demo application.

In the same way we create models/List.js model:

and models/Card.js model:

Let’s add User model, models/User.js

Note the private: true property of tokens array. That property means that the field will never be exposed to the GraphQL schema. In tokens array we will store information in following way:

[{
token: "user-random-token"
}, {
token: "user-random-token-2"
}]

Create relations

Board has many Lists belong to it. Let’s define “has many” relation by adding a following property in properties of the models/Board.js model:

lists: {
relation: 'hasMany',
ref: 'list',
foreignKey: 'boardId'
}

Above definition can be simplified:

lists: {
relation: 'hasMany'
}

In this case, Spikenail will try to guess exact parameters of the relation. So listing of models/Board.js becomes:

Let’s similarly add relations to List and Card models

models/List.js

models/Card.js

Configuring access control

Lets add access rules for Board.js model.

Rules placed in the acl array of the model schema.

The framework processes ACL rules one by one in a natural order. There is no any access restrictions by default, so the first rule is to disallow everything for everyone:

{
allow: false,
roles: '*',
actions: '*'
}

roles and actions fields can be omitted, so the rule becomes:

{
allow: false
}

Next, allow any authenticated users to create boards. Anonymous users can not create boards:

{
allow: true,
actions: 'create',
roles: 'user'
}

Allow the board owner to do everything:

{
allow: true,
roles: 'owner',
actions: '*',
}

Allow everyone (including anonymous roles) to read the board content if board is marked as not private.

{
allow: true,
roles: '*',
actions: 'read',
scope: { isPrivate: false }
}

Note the scope property — the ACL rule will apply only to those documents which match the MongoDB condition defined by it.

Allow to read any board (both private and public) for a user added to a board as an observer or a member:

{
allow: true,
roles: ['observer', 'member'],
actions: 'read'
}

Finally, our model with the ACL rules becomes:

But how exactly observer, member and owner roles are suppose to work? We haven’t added any logic which allows to Spikenail choose the correct behaviour to handle them.

In Spikenail there are some predefined static roles, e.g. user is any authenticated person, anonymous is not authenticated person correspondingly.

And there are also dynamic roles. For the owner role, Spikenail has a following default logic:

currentUser.id == item.userId

But for observer and member we should define our custom logic. It can be done by specifying roles object of model schema. The Board.js model become:

As you see, the cond function should return either a boolean value or a MongoDB condition.

Let’s add an ACL for the List.js

Note that any relation exists does not affect any ACL rule by itself. Relations affect only the way we fetch data.

Disallow everything for everyone:

{
allow: false
}

Allow anyone who can update board to do any action on lists:

{
allow: true,
actions: '*',
roles: '*',
checkRelation: {
name: 'board',
action: 'update'
}
}

Note the checkRelation property — it allows us to determine access control based on another model.

If we don’t want to allow board members to edit the board, e.g. rename it, we should additionally allow them to modify lists as above rules might restrict it:

{
allow: true,
actions: '*',
roles: '*',
checkRelation: {
name: 'board',
roles: ['member'],
action: 'read'
}
}

The model becomes:

Finally, let’s define ACL for Card model.

Disallow everything for everyone:

{
allow: false
}

The one who can update list should be able to do any action on card:

{
allow: true,
actions: '*',
roles: '*',
checkRelation: {
name: 'list',
action: 'update'
}
}

The one who can read list should be able to read card:

{
allow: true,
actions: 'read',
roles: '*',
checkRelation: {
name: 'list',
action: 'read'
}
}

Final Card.js listing:

Adding validations

Usually, the data which we receive from users needs to be validated. It is easy to do with Spikenail.

Lets add some validations for Board model. For example, we want name to be required property and its length to not exceed 50 characters.

This could be done in a following way:

validations: [{
field: 'name',
assert: 'required'
}, {
field: 'name',
assert: 'maxLength',
max: 50
}]

Final Board.js listing:

Run the API

Enter the root directory of your project and run npm start to start the server.

Browse to http://localhost:5000/graphql to open in-browser GraphQL IDE.

If you fetch the code from the spikenail-example-cards repository, you can set SPIKENAIL_EXAMPLE_CARDS_MONGO_CONNECTION_STRING_TEST environment variable and run npm test which clear the specified database and fill it with the test data.

In order to login as some user — change the URL, by adding auth_token http://localhost:5000/graphql?auth_token=igor-secret-token

Let’s do some queries.

Create a board:

mutation {
createBoard(input: { name: "My private board", isPrivate: true }){
board {
id
isPrivate
name
}
errors {
code
field
message
}
}
}

Response:

{
"data": {
"createBoard": {
"board": {
"id": "Ym9hcmQ6NTk2Zjg0M2QxOGE0ZjNlY2VlZTNiN2My",
"isPrivate": true,
"name": "My private board"
},
"errors": null
}
}
}

Querying all lists with cards and board information.

query {
viewer {
allLists {
edges {
node {
id
name
cards {
edges {
node {
title
}
}
}
board {
id
name
}
}
}
}
}
}

Example response:

{
"data": {
"viewer": {
"allLists": {
"edges": [
{
"node": {
"id": "bGlzdDo1OTQ2NmU4Y2M4NzJmMDhjMDgxNTk4ZDM=",
"name": "My List",
"cards": {
"edges": [
{
"node": {
"title": "Card one"
}
},
{
"node": {
"title": "Card two"
}
},
{
"node": {
"title": "Card three"
}
}
]
},
"board": {
"id": "Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjg3",
"name": "My Board"
}
}
},
{
"node": {
"id": "bGlzdDo1OTQ2NmU4Y2M4NzJmMDhjMDgxNTk4ZDU=",
"name": "My second List",
"cards": {
"edges": []
},
"board": {
"id": "Ym9hcmQ6NTkyYmZjOTA2ZjM5Zjc5MGNmNGI5Yjg3",
"name": "My Board"
}
}
}
]
}
}
}
}

Even if it says all, if particular items are not visible by ACL rules, they will be excluded from the response.

Below query will return only those boards which name starts from Public:

query {
viewer {
allBoards(filter: { where: { name: { regexp: "^Public" } }, order: "id DESC" }) {
edges {
node {
id
userId
name
}
}
}
}
}

You can read more about possible queries in the Spikenail documentation.

Conclusion

It was very easy to create complex GraphQL API with Spikenail. Everything was done in the configuration, almost no coding was needed.

There is still a lot of work needs to be done with Spikenail before it will be ready for production.

If you like the idea and the conception please star the repository on GitHub. Any feedback would be greatly appreciated.

--

--