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.
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 = 3000app.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 postgrescreate 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 ../layoutblock 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 ../layoutblock 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 ../layoutblock 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 ../layoutblock 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.