API app with a frontend

23 min. read

What does setting up a Ruby fullstack app looks like in 2022?

Whenever I had worked on this type of app in my work history, it was always on codebases that were already started by others, and had been running in production for a couple of years. I wanted to check out the state of things in 2022. How would I start one of those from scratch today.

I also always wanted to create a proof of concept for a webapp that is both an API and has a sandbox to try it out and a docs page all in the same place. So I created an API app using Sinatra and PostgreSQL, with a frontend that makes AJAX requests to the API using vanilla Javascript. You can see the project details here.

Screenshot of an API app with a frontend

The backend was by far the easiest part to do. As it happens in general with any fullstack app, the backend has the language and library dependencies pinned to a specific version and it will run in a single specific server on a specific operating system etc. Meanwhile, the frontend is a mess; you never know which device will your site be loaded on, which browser and version, and which O.S and version. On top of that, there is a lot of optimization and best practices to keep in mind, like uglifying, minimizing, gzipping, saving for web, optimizing, and what not.

Because of this I keep scafolds for different languages that I can just build apps on top of, and be reassured that they are taking into account all the things. In this particular case, since it's a Ruby app with a vanilla JS frontend, I used both my Ruby scafold and my HTML scafold repositories.

Backend

For the backend, I used my Ruby scafold repository as a starting point. It comes with a basic folder structure for a vanilla Ruby app, RSpec, Rubocop and a couple of badge configurations to keep an eye on code quality and dependencies.

Heroku

As all my hobby apps, this one is deployed to Heroku. I like how easy the process is, and the fact that you can use git to deploy, but also can manage your app from your local console using the heroku-cli tool.

Heroku will automatically detect that your site is a Ruby app. However if you have another stack like Node, you need to specify it like this:


heroku buildpacks:add --index 1 heroku/ruby
heroku buildpacks:add --index 2 heroku/nodejs

Heroku also needs you to add a Procfile and a server that supports parallelism like puma.

Database

I needed a database for the API, so I chose PostgreSQL. For communicating with PostgreSQL, I am using the sinatra-activerecord gem. I could probably be done with just the pg gem and do things manually, but this is a pet project so I was lazy! In particular, I didn't want to implement migrations manually 😅.

To use the gem, you need to add this to your Rakefile:


require 'sinatra/activerecord/rake'

namespace :db do
  task(:load_config) { require './lib/apiapp' }
end

And this to your controller:


require 'sinatra'
require 'sinatra/activerecord'

class APIapp < Sinatra::Base
  register Sinatra::ActiveRecordExtension
  # ...etc.
end

The README specifies how to setup the database locally. There is a specific database setup file for the CI. Back in the day I used travis as my CI but they stopped being free for hobby projects so I switched to the Gitlab CI. I added a step where I copy that file into the default file:


cp config/database.gitlab.yml config/database.yml

I have just three tables at the moment. I wasn't sure about the need for the ranks table, but thought it may scale better to have it from the beginning. I have a seeds.rb file that populates the database with the right ranks at the very beginning of the databse setup process so that they are available from the start:

Web server

For this app I needed a simple web server to serve some endpoints and return JSON, but also return HTML for the sandbox and docs in the frontend.

I am using Sinatra as a thin and simple server. Since Sinatra is a rack app, I added the rack-test gem to the dev dependencies so I can test it nicely. It allows you to write tests like:


it 'loads "Try it!" by default' do
  get '/'
  expect(last_response).to be_ok
  expect(last_response.body).to include('Try it!')
end

Just add it to your spec_helper.rb:


require 'rack/test'

RSpec.configure do |config|
  config.include Rack::Test::Methods
end

Since I have backend and frontend endpoints, I decided to keep those separated in two different Sinatra controllers:

  • Webapp: the frontend controller, and
  • APIapp: the API controller, which has all the API endpoints.

I can tell rack about them in the config.ru file:


run Rack::URLMap.new(
  '/' => Webapp, # Frontend will be mounted at /
  '/api/v1' => APIapp # The API will be mounted at /api/v1
)

Since it's just a proof of concept and I don't plan to take this to the next level, the API is a simple role player management app. Maybe it could be used to build a community of role game players and keep track of their games.

The API

I created three models to represent the three tables in the database. There are some basic validations and helper methods in them. There is a lot of room for improvement as this is just a pet project and also a proof of concept. So some design decisions can be polished.

The API controller contains all the endpoints of the API, at the moment just two GET and one POST. More endpoints can be added as needed.

I could use the factory-bot gem for the test fixtures, but I felt there was no need to add another dependency when my fixtures are very simple. So I just added them all in a spec/factories.rb file.

There is also another gem to cleanup the database after every test, but again, why add another dependency when you can just add this to your spec_helper.rb file:


RSpec.configure do |config|
  config.before(:each) do
    PG.connect(dbname: 'playersapi_test').exec('truncate players, games, ranks;')
  end
end

Frontend

For the frontend, I used my HTML scafold repository as a starting point. It sets up a vanilla JavaScript webapp using Jasmine standalone as the testing framework, and gulp as a task runner. For the CSS I am still using SCSS, although CSS these days is pretty powerful. But I think both modern CSS and SCSS serve different purposes and a mix of both is a better solution.

For this app I decided to keep the frontend assets in a dev folder called assets and have gulp spit the assets ready to serve in a public folder. This is by default the folder that Sinatra uses to serve static assets from, although you can change it if you want. Then I can tell gulp to watch for changes to the assets folder and rerun the relevant tasks:


function watch() {
  gulp.watch(dev.img,  gulp.series(img, cache, reload));
  gulp.watch(dev.js,   gulp.series(lintJS, js, cache, reload));
  gulp.watch(dev.scss, gulp.series(scss, cache, reload));
  gulp.watch(dev.spec, gulp.series(lintSpec));
}

The cache task adds a small string at the end of asset files that have changed, so that the browser pulls those files again when they changed instead of getting them from the browser's cache.


function cache() {
  let token = new Date().getTime();
  return gulp
    .src('./views/layout.erb')
    .pipe(replace(/cachebust=\d+/g, 'cachebust=' + token))
    .pipe(gulp.dest('./views/'));
}

Then in the HTML:


<link rel="apple-touch-icon" href="img/favicon.png?cachebust=1647473925110">
<link rel="stylesheet" href="css/main.css?cachebust=1647473925110">
<script src="js/main.js?cachebust=1647473925110"></script>
...etc.

Stylesheets

I always use normalize as my reset stylesheet. I don't like declarations like * { box-sizing: border-box } and similar. First, you are targeting every single element in the page. Second, you may not need that declaration on absolutely every element in the page! I feel like the contents of normalize are well researched and justified, it adds only the bare minimum that is needed.

Then I build my styles on top of the defaults from HTML5 boilerplate. Again, I feel like they have done the cross-browser research that I would be too lazy to do on my own.

Other things that I do often and are contemplated in my scafold are: creating sourcemaps and adding the right vendor prefixes using autoprefixer.

I tell gulp to uglify all the files into one stylesheet and keep an eye on its size so that it doesn't grow beyond some tens of KB. The whole site I definitely try to keep under 500kB as much as possible, and definitely no more than 1MB.


function scss() {
  del.sync(`${dist.css}**`);
  return gulp
    .src(dev.scss)
    .pipe(sourcemaps.init())
    .pipe(sass({ outputStyle: 'compressed' }))
    .pipe(autoprefixer())
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(dist.css));
}

Javascripts

I tend to write vanilla JavaScript as a start, and only if I need a framework later, I will add it, after doing my research on what's the best framework for the job. I've been making websites for so many years, so I really despise the idea of starting a project with React right away, no matter what. You may not need it!

I usually have one or several UI classes to handle the DOM, and then model my domain with extra classes containing the relevant business logic not related to DOM operations. For this app I ended up with:

  • client.js: The client that does requests to the API endpoints from the frontend
  • selectors.js: I had many selectors on this site so I decided to keep them all organized in one place.
  • ui.js: The class dealing with DOM operations.
  • main.js: The class sitting at the top of everything, creates all the instances and wires things up.
  • prism.js: Lea Verou's great syntax highlighter. The best highlighter.

This all together is less than 20KB. I don't know what it would be like had I used a framework.

There is room for improvement as the blocks for every endpoint in the UI are very similar, so there is some abstraction lurking in there to represent an endpoint block. But that's for future me.

Nothing is installed globally, not even gulp. I just don't like that idea, as different devs may have different versions of gulp installed locally. So everything is pinned to a specific version. I use the package.json file to alias interesting CLI commands:


"scripts": {
  "assets": "node node_modules/gulp/bin/gulp.js assets",
  "start": "node node_modules/gulp/bin/gulp.js"
}

For convenience I created two rake tasks to run these commands:


task(:watch) { sh('npm start') }
task(:assets) { sh('npm run assets') }

In this project I am writing modern JavaScript and not using Babel to transpile it to supported JavaScript. I wouldn't do this in a real project though. In this app I am only using Babel as a parser for ESlint, the linter of my choice. I do concatenate and uglify the JavaScript and create sourcemaps for it:


function js() {
  del.sync(`${dist.js}**`);
  return gulp
    .src(dev.js)
    .pipe(sourcemaps.init())
    .pipe(concat('main.js'))
    .pipe(uglify())
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(dist.js));
}

For the tests, I use the standalone Jasmine library, which is supposed to be used for testing in the browser. I think if your JavaScript runs in a browser it makes more sense to run your tests in a browser too, so you can do cross-browser testing. If you want to run your web tests on a CI, you will need a tool like testem, which lets you configure headless browsers to test in the CI. I didn't do it for this project but have done it in other projects.

The gulp file uses browser-sync to spin up a server that serves the Jasmine tests page:


function server() {
  browsersync.init({
    server: { routes: { '/tests': './assets/js' } },
    port:   4000,
    notify: false,
    open:   false
  });
}

It also have live reload so that it refreshes the page when changes happen:


function reload(callback) {
  browsersync.reload();
  callback();
}

Images

Images used to be the performance elephant in the room until the JavaScript part of things became bloated with huge and slow frameworks, and now JavaScript is the elephant in the room (hehe).

Still, as much as possible I try to use SVGs for images that are either logos, icons or similar. Then I use PNG for images with plain shapes and colors, and JPG for raster images. They can all benefit from optimization for web, for which there are many tools out there. For gulp there is gulp-imagemin, although I am not using it here. Then you can also serve different image sizes for different device sizes, and take into account art-direction (horizontal or vertical orientation), etc. I didn't implement any of that as this is a simple app.

I love SVGs because not only they are more zippable than images, as they are 100% text, but they are also crispier at any size, as they are vectors. They are also very easy to use as sprites. You can put all your SVGs in a file, under the <defs> tag and give each <symbol> an id and viewbox (and a <title>, for accessibility), like this:


<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <symbol id="arrowup" viewBox="0 0 32 32">
      <title>Arrow up icon</title>
      <g fill="hsl(184, 70%, 43%)">
        <path d="M808.7 259.9c23.2-23.6 55.6-35.9 88.6-33.6 31.9-2.4 63 10 84.6 33.6l548.3 546.3c45.4 45.4 45.4 119 0 164.4-45.4 45.4-119 45.4-164.4 0l-354.4-354.4v833.6c.4 63.8-51 115.8-114.8 116.1h-.7c-64-.4-115.8-52.1-116.1-116.1V616.3L426.2 970.6c-45.4 45.4-119 45.4-164.4 0-45.4-45.4-45.4-119 0-164.4l546.9-546.3z"/>
      </g>
    </symbol>
  </defs>
</svg>

Then you can use it as an inline image in your HTML like this, which allows you to style the icon from your CSS through a class if you need to:


<svg aria-hidden="true" class="optional-class">
  <use href="img/icons.svg#arrowup-icon"></use>
</svg>

The file will be cached and all the icons will be immediately available. And since it's a 100% text file, the size will be low (and even lower with server gzipping).

Comments