Standards are good, they make our lives easier. Graphiti and it's client library Spraypaint make it easy to build JSON:API compliant APIs that integrate seamlessly with front-end frameworks like Vue.
I’m using graphiti in a production application to serve JSON requests to Vue components embedded in our Rails views. It's been reliable, flexible and a pleasure to use.
In this tutorial we’ll walk through setting up Vue as an SPA with a JSON:API compliant Rails 5 API using graphiti. You can clone the demo app to see the finished product.
# follow along
git clone git@github.com:mikeeus/demos-rails-webpack.git
cd demos-rails-webpack
git checkout ma-vue-graphiti
Set up Rails API with Webpacker
Create Rails app with webpacker and Vue. I use postgresql but you can use any database you like.
mkdir rails-vue
rails new . --webpack=vue —database=postgresql
rails db:create db:migrate
And… done! That was easy right? Now we can move on to setting up graphiti to handle parsing and serializing our records according to the JSON API spec.
Set up Graphiti
Install graphiti, you can find the full instructions in the docs. We’ll need to add the following gems.
# The only strictly-required gem
gem 'graphiti'
# For automatic ActiveRecord pagination
gem 'kaminari'
# Test-specific gems
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker'
gem 'graphiti_spec_helpers'
end
group :test do
gem 'database_cleaner'
end
We’ll need to add Graphiti::Rails
to our Application controller so graphiti can handle parsing and serializing our requests. And we’ll register the Graphiti::Errors::RecordNotFound
exception so we can return 404.
# app/application_controller.rb
class ApplicationController < ActionController::API
include Graphiti::Rails
# When #show action does not find record, return 404
register_exception Graphiti::Errors::RecordNotFound, status: 404
rescue_from Exception do |e|
handle_exception(e)
end
end
Now lets create a Post model.
rails g model Post title:string content:string
rails db:migrate
We’ll also need to create a PostResource for graphiti and a controller to handle requests. Graphiti has a generator that makes it easy to set this up.
rails g graphiti:resource Post -a index
We’re going to declare our attributes and add ActionView::Helpers::TextHelper
to format our Post content using simple_format
so we can render it nicely on our client.
class PostResource < Graphiti::Resource
include ActionView::Helpers::TextHelper
self.adapter = Graphiti::Adapters::ActiveRecord
primary_endpoint '/api/v1/posts'
attribute :title, :string
attribute :content, :string do
simple_format(@object.content)
end
end
The generator will also create specs and a controller at app/controllers/posts_controller.rb
. We’re going to move that to a namespaced folder app/api/v1
which will allow us to manage API versions in the future.
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < ApplicationController
def index
posts = PostResource.all(params)
render jsonapi: posts
end
end
end
end
We use render jsonapi: posts
to render the posts according to the JSON:API spec so we can parse it on our client using graphiti's js client spraypaint.
Now let’s add the route.
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :posts, only: :index
end
end
end
Alright looking good! All we need now is a client to consume our API.
Setup Vue
Webpacker comes with a generator for vue which you can see in the docs. It makes it super easy to add Vue or any other front-end framework like React or Angular to our application.
bundle exec rails webpacker:install:vue
Running the above will generate files at app/javascript
We’re going to edit app/javascript/packs/application.js
so that we can render our App.vue component.
// app/javascript/packs/application.js
import Vue from 'vue/dist/vue.esm'
import App from '../app.vue'
document.addEventListener('DOMContentLoaded', () => {
const app = new Vue({
el: '#app',
components: { App }
})
})
For now we can disregard the Vue component, we’ll fill it in later once we’ve setup our resources and endpoints.
Setup Rails to Serve Static Content
We can’t use our ApplicationController to serve our index.html
page since it inherits from ActionController::Api
and we want to keep it that way since our other controllers will inherit from it.
In order to serve our index page for the SPA, we’ll use a PagesController
that inherits from ActionController::Base
so it can serve html files without issue.
# app/pages_controller.rb
class PagesController < ActionController::Base
def index
end
end
Next we’ll add a route for our homepage and redirect all 404 requests to it so our SPA can take care of business.
# config/routes.rb
Rails.application.routes.draw do
root 'pages#index'
get '404', to: 'pages#index'
namespace :api do
# ...
end
end
Looking good, friends! Now let’s add our index.html page which will render our Vue component.
# app/views/pages/index.html
<%= javascript_pack_tag 'application' %>
<%= stylesheet_pack_tag 'application' %>
<div id="app">
<app></app>
</div>
It’s super simple: it just pulls in our javascript and stylesheets compiled by webpacker. Then we add a div with id=“app” and a nested <app></app>
so our Vue wrapper component can pick it up and render the main component.
This is the only Rails view we need to write for our application to work.
Create Models on Client
Usually when I build an SPA I’ll write services that use libraries like axios to make Ajax requests to the backend. Graphiti comes with a client library called spraypaint that handles parsing and serializing JSON:API payloads. It supports including associations, advanced filtering, sorting, statistics and more.
Let’s set it up!
yarn add spraypaint isomorphic-fetch
Next let’s create an ApplicationRecord
class that will store our spraypaint configuration.
// app/javascript/models/application_record.js
import { SpraypaintBase } from 'spraypaint';
export const ApplicationRecord = SpraypaintBase.extend({
static: {
baseUrl: '',
apiNamespace: '/api/v1',
}
})
We set the baseUrl
and apiNamespace
to ‘’ and ‘/api/v1‘ respectively so that spraypaint uses relative paths and avoids CORS requests. It also namespaces our requests so we can manage API versions easily.
Now the Post model
// app/javascript/models/post.model.js
import { ApplicationRecord } from './application_record';
export const Post = ApplicationRecord.extend({
static: {
jsonapiType: 'posts'
},
attrs: {
id: attr(),
title: attr(),
content: attr()
},
methods: {
preview() {
return this.content.slice(0, 50).trim() + '...'
}
}
})
We declare the id
, title
and content
attributes. We also add a method to return a truncated preview of the content to show how we declare methods.
The jsonapiType
property is needed to generate the endpoint and parse and serialize the JSON payload.
Now we’re ready to hook up the client to the API.
Hook up SPA
To hook everything up we’ll create a Vue component that uses our spraypaint models to communicate with our endpoints.
// app/javascript/app.vue
<template>
<div>
<h1>Posts</h1>
<div v-if="error" class="error">{{error}}</div>
<div class="loading" v-if="loading">Loading...</div>
<ul>
<li v-for="post of posts" :key="post.id">
<h3>{{post.title}}</h3>
<p v-html="post.preview()"></p>
</li>
</ul>
</div>
</template>
<script>
import {Post} from './models/post.model'
export default {
data: function () {
return {
posts: [],
error: null,
loading: true
}
},
created() {
Post.all()
.then(res => {
this.posts = res.data
this.loading = false
})
.catch(err => {
this.error = err
this.loading = false
})
}
}
</script>
<style scoped>
h1 {
text-align: center;
}
ul {
list-style: none;
}
</style>
Marvellous! If we add some posts in the console and run the application we will see the posts load and render in the page.
Notice that we import our Post model and use it in our created()
call like it was a Rails model. Calling Post.all()
returns a promise that we can chain to set our posts
and loading
data properties. The spraypaint model can chain more useful methods like where
and page
.
Post.where({ search: 'excerpt' })
.stats({ total: 'count' })
.page(1)
.per(10)
.order({ created_at: 'desc' })
.all()
.then(res => ...)
Spraypaint is a very powerful library that supports pagination, sorting, statistics, complex filtering and much more. You can check out the spraypaint docs for detailed guides.
Conclusion
Standards are good. Vue is awesome, and so is Rails. Gems and libraries like Graphiti and Spraypaint make it super easy to build scalable REST APIs that comply with said standards and integrate seamlessly with frameworks like Vue.
I hope you enjoyed the article, don’t forget to like if you did. I would love to hear your thoughts or suggestions for other articles. Just leave a comment below :)
Top comments (1)
Hello,
so I have been working on vue and jsorm/spraypaint for some time and i am stuck with, like how to make a fetch request instead of Post and send some metadata also with it.
like send data to customer/:id, where the id would be fetched from the url and send as meta data, while secrets from the URL to be send as normal data.
import { ApplicationRecord } from './application_record';
export const Post = ApplicationRecord.extend({
static: {
jsonapiType: 'posts/:id'
},
})