In this article, I'm going to walk you through building a proof of concept blog powered by Slash GraphQL.
Graph databases are a fascinating way to model the information in a system
where the relationships (edges) between pieces of data (nodes) are first-class
entities of the system. This is a different approach to the more common
relational database (RDBMS) model, where the relationships between records are
implied. For example, "this user owns this post because user.id == post.user_id"
Working with graph databases can feel a little unfamiliar if you're used to working with RDBMSs, so in this article I'll try to show you some of the basics. As usual, I'm not trying to produce finished, production-ready code here - just simple examples to show you the underlying concepts.
You can find all the example code for this article here.
For this article, I'm going to use dgraph, a native GraphQL database (as opposed to a graphql layer on top of a relational datastore) written in Go, and designed for large-scale application with high-availability and transaction support. If you're interested in learning more about the project, they have some good introductory videos on their YouTube Channel.
Rather than installing dgraph on a server, or running it locally via docker, I'm going to use their hosted GraphQL backend service Slash GraphQL. The free tier (at time of writing) allows 20,000 operations per month, which is plenty for our purposes.
Let's get started.
In order to use Slash GraphQL, you need to login using your GitHub or Google account, and you'll then see the web interface with options to create and manage your backends, as well as lots of links to tutorials and other documentation, which I recommend exploring.
Dgraph recently released slash-graphql, a command-line tool for managing Slash GraphQL backends, and because I strongly prefer working on the command line, I'm going to use that for this article. But you can do everything via the web interface, if you prefer.
You can install the command line tool by running:
npm install --global slash-graphql
Using slash-graphql to manage Slash GraphQL backends feels a lot like using
the heroku command-line tool to manage Heroku applications, so if you're
comfortable with that, you should find this quite familiar.
To create a Slash GraphQL backend via the command-line, we first have to login:
slash-graphql login
This will prompt you to confirm that a code on your terminal matches a code shown on the web page that the command will open. Once you have done this, you can issue commands to Slash GraphQL.
Now we can create our graphql backend like this:
slash-graphql create-backend blog
This will output the "endpoint" of your backend, which usually takes around 20 seconds to create. This is the URL we will use to interact with our graphql database. You'll be using this throughout this article, so please make a note of it.
In my case, my graphql endpoint is:
https://anxious-aunt.us-west-2.aws.cloud.dgraph.io/graphql
Please substitute your own endpoint wherever you see this in the code examples.
We need to define a schema for our blog data.
type User {
email: String! @id @search(by: [hash])
name: String @search(by: [exact])
posts: [Post] @hasInverse(field: user)
}
type Post {
id: ID!
title: String! @search(by: [fulltext])
body: String!
image: String
user: User!
comments: [Comment] @hasInverse(field: post)
}
type Comment {
id: ID!
body: String!
user: User!
post: Post!
}
Let's break that down a little. I'm just going to cover a few highlights here. For details, please have a look at the Slash GraphQL documentation.
Starting with the last entity, we define Comment like this:
type Comment {
id: ID!
body: String!
user: User!
post: Post!
}
This means we have a node type called Comment, which has several attributes.
-
id: ID!tells dgraph to generate its own uid values for this type -
bodyis a required string value -
postanduserhave typesPostandUser, and the!indicates that these are required fields. i.e. every comment belongs to a Post, and also belongs to a User.
The Post type has a comments attribute which is a list of Comment nodes:
comments: [Comment] @hasInverse(field: post)
Similarly, User has many posts.
Note that we're telling our backend what node types exist, and how they relate to each other, but we don't have to define how to make those relationships work - no foreign keys, no joins, and no many-to-many mapping tables.
We can apply this schema to our backend like this:
slash-graphql update-schema --endpoint https://anxious-aunt.us-west-2.aws.cloud.dgraph.io/graphql schema.graphql
Now that we have our schema, we can add some data.
Although the slash-graphql tool has an import-data function, that's
designed for restoring a backup rather than adding chunks of data.
The usual way of interacting with a graphql database is via HTTP requests, so
that's how we're going to add our data using curl, via this script:
#!/bin/bash
FILE=$1
curl --request POST \
--header "Content-Type: application/graphql" \
--header "Authorization: bearer ${API_TOKEN}" \
--data "$(cat ${FILE})" \
${ENDPOINT}
The script uses curl to make an HTTP POST request (--request POST) to our graphql endpoint ${ENDPOINT}.
The body of the request is the contents of the file ${FILE} (--data $(cat "${FILE})"), and we pass the name of the file as an argument to the script FILE=$1.
We set the Content-Type header to application/graphql (it is also possible to send commands in JSON).
We also need to provide an API token to the backend, hence the Authorization header value of bearer ${API_TOKEN}.
We need to use the Slash GraphQL web interface to generate an API token for our application.
In the interface, go to: Dashboard > Settings > Security > Add API Key
Give your token a name, e.g. "blog middleware", and be sure to keep a copy of the value which is displayed. You won't be able to see it again.
Now we can set the environment variables we need in our post.sh script:
export ENDPOINT=https://anxious-aunt.us-west-2.aws.cloud.dgraph.io/graphql
export API_TOKEN=[your API token value]Now we can add our data.
Here is the graphql we're going to send (a mutation is how you alter data):
mutation {
addPost(input: [
{
user: {
name: "Burk Dronsfield",
email: "bdronsfield0@apple.com"
},
title: "Scallops - 10/20",
image: "https://picsum.photos/id/441/600/300",
body: "Vestibulum ac est lacinia nisi venenatis tristique. Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat.\n\nIn congue. Etiam justo. Etiam pretium iaculis justo.",
comments: [
{
body: "First post!",
user: {
email: "tgodleman1@chronoengine.com",
name: "Tamar Godleman"
}
}
]
}
])
{
post {
id
title
}
}
}
None of those users are real people, it's sample data from mockaroo.
Notice how we're adding two users, a blog post, and a comment, all with a single request to our backend. This is where we can start to see the power of graphql databases.
./post.sh addData.graphql | jqThe
| jqis optional, but the response to our post will be a JSON document, and piping it through jq makes it easier to read.
We can query the database in exactly the same way, via an HTTP POST. Here's a
query to retrieve users and the titles of any posts they've written:
query {
queryUser {
name
posts {
title
}
}
}
We run this query in exactly the same way as the mutation:
./post.sh queryUser.graphql | jqThe top of the output you get should be something like this:
{
"data": {
"queryUser": [
{
"name": "Tamar Godleman",
"posts": []
},
{
"name": "Burk Dronsfield",
"posts": [
{
"title": "Scallops - 10/20"
}
]
}
]
},Now that we can manipulate data in our graphql database, let's put together a small middleware layer to handle HTTP GET/POST requests from a web browser, and make graphql requests to our backend.
There are lots of different architectures you could use here. For instance, you could have a single-page app. using something like React to make graphql requests directly from the browser. I'm using this architecture because I want to keep this article simple, and I think it's an easy design to understand.
Here's how it's going to work:
+--------------+ +-------------+ +---------+
| | | | | |
| |------>| |-------->| |
| Web browser | HTTP | Web server | GraphQL | Slash |
| |<------| |<--------| GraphQL |
| | | | | {s}|
+--------------+ +-------------+ +---------+
This github repository contains a very simple express.js webserver. To run it, just clone the repository and launch the server:
git clone https://github.com/digitalronin/graphql-blog-express
cd graphql-blog-express
export ENDPOINT=https://anxious-aunt.us-west-2.aws.cloud.dgraph.io/graphql
export API_TOKEN=[your API token value]
npm install
node index.js
At this point, you can visit https://localhost:5000 to see the web interface.
Or, if you want to run it on Heroku (and you have the heroku command-line tool installed and logged in):
git clone https://github.com/digitalronin/graphql-blog-express
cd graphql-blog-express
heroku create
heroku config:set ENDPOINT=https://anxious-aunt.us-west-2.aws.cloud.dgraph.io/graphql
heroku config:set API_TOKEN=[your API token value]
git push heroku master
heroku open
This should launch a web browser showing you the interface.
You should see a very ugly page showing a list of the blog posts (only one, so far), and you should be able to click on the titles to view each post.
Let's see how the code works. At the bottom of the index.js file, we've got this:
async function graphqlQuery(graphql) {
const result = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/graphql",
Authorization: `bearer ${API_TOKEN}`
},
body: graphql
});
const obj = await result.json();
console.log("obj:", obj);
return obj;
}This is the heart of our middleware application - a function that takes a string of graphql, posts it to the endpoint and returns a data object from the JSON response.
I've deliberately kept this code to the simplest form that works. There are a lot of cleverer ways to interact with a graphql backend, and I'd encourage you to read some of the Slash GraphQL documentation to find out more.
The list of blog posts comes from this function:
async function listPosts(req, res) {
const obj = await graphqlQuery(`
query {
queryPost {
id
title
}
}
`);
res.render("pages/posts", { posts: obj.data.queryPost });
}And this one displays the individual posts:
async function showPost(id, req, res) {
const obj = await graphqlQuery(`
query {
queryPost(filter: {
id: ["${id}"]
}) {
title
image
body
user {
name
}
comments {
body
user {
name
}
}
}
}
`);
res.render("pages/post", { post: obj.data.queryPost[0] });
}The addPost and addComment functions have a very similar structure, but
send mutations rather than queries to the backend, and there's a very basic
interface using HTML forms to wire those up.
I've added name and email fields to every form, but of course in a real application you'd have signup and authentication steps, and read the user credentials from a cookie.
The web application is very basic - just enough to demonstrate the concepts. In particular, please never use unsanitised user input in a real application, because it's a security nightmare.
We've talked briefly about the difference between a graph database and a traditional RDBMS, and we've gone through the process of setting up a hosted graphql backend with Slash GraphQL, creating a schema and using mutations and queries to manipulate your data. Finally, we put together a very basic web interface as a proof of concept for a graphql-powered blog.
I've barely scratched the surface of what you can do with a graphql database in this article, but I hope it inspires you to dive in and learn more.