Creating advanced GraphQL API quickly using Spikenail
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.