Node.js for Rails Lovers

You love Ruby on Rails. There’s no easier or fun way to build a web application. But maybe you have several projects with specific Node.js business needs, or you just want to learn a new framework. You’re well-versed in JavaScript, so Node should be an easy transition, right? Let’s find out.

Craig Phares
Jun 16 · 8 min read

TLDR

Want to see the finished project? Check out the GitHub repo.

Convention over configuration

With Rails, you don’t have to think about how to structure your app. According to The Rails Doctrine, “You’re not a beautiful and unique snowflake”. There’s no need to deliberate over the same decisions for every single project. Conventions significantly accelerate productivity. Instead of focusing on mundane plumbing, you can focus on the extraordinary features of your app.

If you want to stick to this philosophy, read no further. Go install Sails.js, which is essentially Rails for Node, and get building.

Sometimes you need something more lightweight or custom than what you’re given with Rails or Sails. If you want to build your own Node app from scratch, read on to see how to piece together all the basics for a streamlined MVC web app.

Decision paralysis is a real thing

Coming from the Rails world where everything is already decided for you, it seems like there’s an endless supply of possible components, file structures, and naming conventions to use in a Node app. It’s easy to belabor over the number of GitHub stars, developer articles, and StackOverflow comments for each potential library. In Rails, you get ActiveRecord. In Node, you get to choose which ORM, if any, to use. In Rails, your controllers go in the controllers directory. In Node, they go wherever you want. How can someone ever get started in this mess of decisions?

In this article, I’ve made some decisions for you. Of course, this isn’t the only way (or probably even the best way) to build a Node app. But it’s a good start, and each component can be swapped with an alternative. What follows is a basic boilerplate to use for real-world Node apps, in the form of a blog.

Note: commands below assume you’re using MacOS. There’s a link to each library, where command line instructions are available for all platforms.

The engine

What do you need in order to get started with Node? You need Node. So let’s install it. It’s a good idea to use a version manager for Node, just like you use RVM or rbenv for Ruby. Install NVM.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

Then you can install the latest version of Node (or whatever version you like).

nvm install node

Dependency management

Just like Rails has gems, Node has JavaScript packages. They’ll all be installed under a node_modules folder in your working directory. Instead of Bundler, Node ships with npm, and we’ll be installing global packages with npm. But for our project packages, we’re going to use Yarn, which is slightly faster and strongly binds package versions. Yarn can be installed with Homebrew.

brew install yarn

Once Yarn is installed, you can initialize your new blog project easily.

mkdir blog
cd blog
yarn init

Follow the prompts, and you should now have a package.json file in your project directory.

The framework

Rails is a framework. Ruby is its language. The most popular Node framework is Express. It’s about as minimalist as you can get, and you can use it however you like. Let’s install Express with Yarn.

yarn add express

Here’s the simplest Express app in the world. Create an index.js file at your project’s root.

// index.js
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => res.send('I am Node!'))app.listen(port, () => console.log(`My blog is listening on port ${port}!`))

Add a start script to your package.json file.

// package.js
{
...
"scripts": {
"start": "node index.js"
}
...
}

Then startup your awesome app.

yarn start

You should see “My blog is listening on port 3000” in your console. Browse to http://localhost:3000, and you’ll see “I am Node!” in your browser. Congratulations, you just launched your first Node app!

The model

Well, that’s not much of an app. Let’s give it some substance. Most web applications are backed by some persistent data.

The Database

Just like Rails, Node will let you connect any database you like. Here we’re going to use PostgreSQL, but this could be any database.

Many Node tutorials you find online will use MongoDB, which is a viable document-based database. But we’re building a relational database today, so we’re sticking with SQL.

Install PostgreSQL if you don’t already have it, and create your development database.

brew install postgresql
psql postgres
create database blog_development;
\q

Finally, add the PostgreSQL dependency to your project.

yarn add pg

The query interface

Rails gives you Active Record Query Interface out of the box. Sure, you could write SQL commands, but who wants to do that? We will use Knex.js for our query interface, a popular SQL query builder. Add knex to your project.

yarn add knex

Knex gives you all sorts of control over your database. We’re going to use its command line interface for setup, migrations, and seeds. Install the knex CLI globally with npm, and then initialize knex in our project.

npm install knex -g
knex init

That will create knexfile.js in your project directory, which holds our database configurations. Let’s make it ours.

// knexfile.js
module.exports = {
development: {
client: 'pg',
connection: {
database: 'blog_development'
}
}
}

Now let’s create some migrations.

knex migrate:make create_articles

This will generate a new migration file inside a migrations folder at the root of your project. Edit it to define our new articles table.

// migrations/XXXXXXXXXXXXXX_create_articles.js
exports.up = knex => knex.schema
.createTable('articles', (table) => {
table.increments('id').primary()
table.string('title')
table.text('text')
table.timestamps()
})
exports.down = knex => knex.schema
.dropTableIfExists('articles')

Run the migration, and you now have an articles table in your database.

knex migrate:latest

ORM

Object-relational mapping helps to work with relations between data sets. Rails give you Active Record. Node lets you choose. We’re going to use Objection.js, which is supported by knex. Add it to your project with Yarn.

yarn add objection

Now you can put all your models in a models folder. Create a base model from which all other models will inherit. This will handle our timestamps.

// models/BaseModel.js
const { Model } = require('objection')
class BaseModel extends Model {
$beforeInsert(context) {
super.$beforeInsert(context)
const timestamp = new Date().toISOString()
this.created_at = timestamp
this.updated_at = timestamp
}
$beforeUpdate(opt, context) {
super.$beforeUpdate(opt, context)
this.updated_at = new Date().toISOString()
}
}
module.exports = BaseModel

And add a model for our articles.

// models/Article.js
const BaseModel = require('./BaseModel')
class Article extends BaseModel {
static get tableName() {
return 'articles'
}
}
module.exports = Article

Finally, we can hook everything together in our Node app. This will load your database configuration, and initialize Objection.js with those settings.

// index.js
...
const knex = require('knex')
const { Model } = require('objection')
const knexConfig = require('./knexfile')
const db = knex(knexConfig[process.env.NODE_ENV || 'development'])
Model.knex(db)
...

The view

Views in Node don’t veer too far from what you’re used to in Rails. We can use the same naming convention, and keep all our views in a views directory. To render views, Express requires a template engine. There are tons of template engines available. We’re going to use Pug. Install it with Yarn.

yarn add pug

And tell Express to use it.

// index.js
...
app.set('view engine', 'pug')
...

Add a global layout template.

// views/layout.pug
html
head
title= title
body
block content

Now we can build all our CRUD views for our articles.

List all articles:

// views/articles/index.pug
extends ../layout
block content
h2 Articles
a(href='/articles/new') New article each myArticle in articles
article
a(href=`/articles/${myArticle.id}`)
h2= myArticle.title
div= myArticle.text
form(method='post' action=`/articles/${myArticle.id}`)
input(type='hidden' name='_method' value='delete')
button Delete

Show an article:

// views/articles/show.pug
extends ../layout
block content
a(href=’/articles’) Articles
h2= article.title
div= article.text
a(href=`/articles/${article.id}/edit`) Edit

Create a new article:

// views/articles/new.pug
extends ../layout
block content
h2 New article
form(method='post' action='/articles') p
label(for='title') Title
input(type='text' id='title' name='title')
p
label(for='text') Text
textarea(id='text' name='text')
button(type='submit') Save
a(href='/articles') Cancel

Edit an article:

// views/articles/edit.pug
extends ../layout
block content
h2= `Edit ${article.title}`
form(method='post' action=`/articles/${article.id}`)
input(type='hidden' name='_method' value='patch')
p
label(for='title') Title
input(type='text' id='title' name='title' value=article.title)
p
label(for='text') Text
textarea(id='text' name='text')= article.text
button(type='submit') Save
a(href=`/articles/${article.id}`) Cancel

In order for our app to handle PATCH and DELETE requests, you’ll need to add some middleware. Install method-override with Yarn.

yarn add method-override

And edit index.js to tell Express to use the middleware. We already added hidden fields in our form that tell Express which method to use.

// index.js
...
const methodOverride = require('method-override')
...
app.use(methodOverride((req, res) => {
if (req.body && typeof req.body === 'object' && '_method' in req.body) {
var method = req.body._method
delete req.body._method
return method
}
}))
...

All our views are set, but we need to route the data to them. That’s where the controller comes in.

The controller

Rails uses a combination of routes.rb and controllers to route requests. You’ve already added a route at the root of your app. Let’s delete that and make this more Rails-y.

Delete the following line from index.js:

app.get('/', (req, res) => res.send('I am Node!'))

Create a new routes directory, so we can add a router. We’re going to use the naming convention of routes instead of controllers to hold all our routing. Add this root router:

// routes/index.js
const express = require('express')
const articlesRouter = require('./articles')
const router = express.Router()router.get('/', (req, res) => res.send('I am Node!'))router.use('/articles', articlesRouter)module.exports = router

And add an articles router to CRUD our articles:

// routes/articles.js
const express = require('express')
const Article = require('../models/Article')
const router = express.Router()// List articles
router.get('/', async (req, res, next) => {
const articles = await Article
.query()
return res.render('articles/index', {
title: 'Articles',
articles
})
})
// Show article
router.get('/:id', async (req, res, next) => {
if (req.params.id === 'new') {
return next()
}
const article = await Article
.query()
.findById(req.params.id)

return res.render('articles/show', {
title: article.title,
article
})
})
// New article
router.get('/new', (req, res, next) => {
return res.render('articles/new', {
title: 'New article'
})
})
// Create an article
router.post('/', async (req, res, next) => {
const title = req.body.title
const text = req.body.text
const article = await Article
.query()
.insert({ title, text })
return res.redirect(`/articles/${article.id}`)
})
// Edit article
router.get('/:id/edit', async (req, res, next) => {
const article = await Article
.query()
.findById(req.params.id)

return res.render('articles/edit', {
title: 'Edit article',
article
})
})
// Update an article
router.patch('/:id', async (req, res, next) => {
const title = req.body.title
const text = req.body.text
await Article
.query()
.findById(req.params.id)
.patch({ title, text })
return res.redirect(`/articles/${req.params.id}`)
})
// Delete an article
router.delete('/:id', async (req, res, next) => {
await Article
.query()
.deleteById(req.params.id)
return res.redirect('/articles')
})
module.exports = router

In order to parse the body of those requests, we need to tell Express to do so. Install the body-parser middleware:

yarn add body-parser

Then edit index.js to define which body parsing to use in your app, and tell Express to use your root router.

// index.js
...
const bodyParser = require('body-parser')
const router = require('./routes')
...
app.use(bodyParser.urlencoded({ extended: false }))
...
app.use(‘/’, router)
...

That’s it! Your app is ready for use. Fire it up with Yarn.

yarn start

Navigate to http://localhost:3000/articles and add some articles to your awesome Node blog.

Summary

With little effort, you now have a solid boilerplate with which to start your Node projects. The great thing about Node apps is you can pick and choose at a very granular level which pieces you want to include, and which to leave out. This setup will bring you very close to what you’re familiar with in Rails.

I hope you found this helpful! There’s lots more to do: testing, validation, relational mapping, security, partials… the list goes on. Leave your comments with follow-ups you’d like to see.

If you came to Node.js from a Rails background, I’d love to hear your thoughts on your own process.

Keep making! 🚀


Craig Phares is the founder of Six Overground, a digital agency in Asbury Park, NJ. His latest side projects include Link My Photos, Keepor, Icotar, CoinQuest, and dropdrop.

Six Overground

Thoughts and experiences in design and technology.

Craig Phares

Written by

Maker, hacker, designer, and purple belt. Founder of sixoverground.com. Working on https://linkmy.photos and dropdrop.it.

Six Overground

Thoughts and experiences in design and technology.