ReadIt a Simple Content Curation platform build with Rails API Backend and Vanilla Javascript

Ruby on Rails is a complete framework for building a full-stack web application used by many big organisations. In this blog post, I am going to be taking you through building an API with Ruby on Rails and consuming the API with a vanilla JavaScript FrontEnd.

The Backend

Building an API with Rails is now a first-hand citizen from Rails 5 that means you can pass in — API flag and get many of the boilerplates removed and get all you need for API only. Getting started with our project. ReadIt is a simple platform that allows users to submit interesting stories they found online.

This will generate a scaffold Ruby on rails app that is API specifics then in the Gemfile I uncomment the ‘rack-cors’ and also added faker for generating fake user content for our seed data. The run bundle install

ruby '2.6.1'# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.0.2', '>='
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'
# Use Puma as the app server
gem 'puma', '~> 4.1'
# Build JSON APIs with ease. Read more:
# gem 'jbuilder', '~> 2.7'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
# gem 'bcrypt', '~> 3.1.7'
# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.2', require: false
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'
gem 'faker'group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
group :development do
gem 'listen', '>= 3.0.5', '< 3.2'
# Spring speeds up development by keeping your application running in the background. Read more:
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

In the config > Initializer folder cors.rb update the file with the following to avoid CORS

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
# Read more: 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]

In this app, we have three models User, Content and Comment. Using Rails Generator

This will generate the User resource which includes Model, Controller and Routes, we can do this for the remaining two resources.

rails g resource comment comment_text:string user:references conntent:references


When you run the migration using rails db:migrate you have the schema bellow

create_table "comments", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "content_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "comment_text"
t.index ["content_id"], name: "index_comments_on_content_id"
t.index ["user_id"], name: "index_comments_on_user_id"
create_table "contents", force: :cascade do |t|
t.string "url"
t.string "description"
t.string "image"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "user_id"
t.string "title"
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
add_foreign_key "comments", "contents"
add_foreign_key "comments", "users"


VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: true
has_many :contents
has_many :comments, through: :contents
class Content < ApplicationRecord
validates :url, presence: true
validates :description, presence: true
validates :image, presence: true
validates :title, presence: true
belongs_to :user
has_many :comments
class Comment < ApplicationRecord
validates :content, presence: true

belongs_to :user
belongs_to :content

Above I added validation and relationships between the models.


Content Controller to render JSON

def show
content = Content.find(params[:id])
if content
render json: {
title: content.title,
url: content.url,
image: content.image,
description: content.description,
user: content.user,
comment: content.comments
render json: {message: "Can't find the content"}
def create
content =
content.title = params[:title]
content.url = params[:url]
content.description = params[:description]
content.image = params[:image]
content.user_id = 1
render json: content
render json: {message: "Error creating a content"}
def destroy
content = Content.find(params[:id])
if content
render json: {message: "Deleted succesfully"}
render json: {message: "Can't find this content"}
endprivatedef content_params
params.permit(:title, :url, :image, :description)

Comment Controller

def create 
content = Content.find_by(id: params[:id])
comment =
comment.content = params[:content]
comment.user_id = 1
comment.content_id =
render json: comment
render json: {message: "Error creating a comment"}

Our API is ready for testing Using postman starting the server with ‘rails s’ and sending a get request to we get this response back

Our API is complete we can move ahead with the front end.

For the frontend, I have this folder structure.

> Asset
-CSS > style.css
- JS > index.js

<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Top Stories from around the web</title><link rel="stylesheet" href="" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="./assets/css/style.css" ></head><body><main role="main" id="main"></main><script src="./assets/js/index.js"></script><script src="" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script></body></html>

All the JavaScript file is located in the index.js and every HTML page is dynamically generated from the JavaScript. I use the Fetch API to make AJAX call to the backend.

// Add formconst formHTML = () => {let formHtml = `<section class="jumbotron text-center"><div class="container"><h1>ReadIt</h1><p class="lead text-muted">Curated top stories from around the web</p><form class="form-content"><img class="mb-4" src="/docs/4.4/assets/brand/bootstrap-solid.svg" alt="" width="72" height="72"><h2 class="h3 mb-3 font-weight-normal">Add a story</h2><div class="form-group"><label for="inputEmail" class="sr-only">Title</label><input type="text" id="inputTitle" class="form-control" placeholder="Title" required autofocus></div><div class="form-group"><label for="inputURL" class="sr-only">Story URL</label><input type="url" id="inputURL" class="form-control" placeholder="URL" required></div><div class="form-group"><label for="inputImage" class="sr-only">Image URL</label><input type="url" id="inputImage" class="form-control" placeholder="Image URL" required></div><div class="form-group"><label for="description" class="sr-only">Description</label><textarea class="form-control" id="description" rows="3" placeholder="Description"></textarea></div><button class="btn btn-lg btn-primary btn-block" type="submit">Submit</button></form></div></section><section class="contents"><div class="album py-5 bg-light"><div class="container"><div class="row"></div></div></div></section>`;main.innerHTML += formHtml;};const contentCard = content => {return `<div class="col-md-4"><div class="card mb-4 shadow-sm"><img  class="bd-placeholder-img card-img-top" src="${content.image}"/><div class="card-body"><h3>${content.title}</h3><p class="card-text">${content.description}</p><div class="d-flex justify-content-between align-items-center"><div class="btn-group"><a class="btn btn-sm btn-outline-secondary" href="${content.url}" target="_blank">View Story from Source</a></div><small class="text-muted">By: ${content.user.first_name}</small></div></div></div></div>`;};// append card to HTMLconst displayCard = content => {const contentRow = document.querySelector(".row");contentRow.innerHTML += contentCard(content);};// Get contentsconst getContents = () => {fetch("http://localhost:3000/contents").then(resp => resp.json()).then(contents => {contents.forEach(content => displayCard(content));});};// Add contentconst addContent = () => {const addContentForm = document.querySelector(".form-content");addContentForm.addEventListener("submit", e => {e.preventDefault();let title = document.querySelector("#inputTitle").value;let url = document.querySelector("#inputURL").value;let image = document.querySelector("#inputImage").value;let description = document.querySelector("#description").value;const contentRow = document.querySelector(".row");let htmlCard = `<div class="col-md-4"><div class="card mb-4 shadow-sm"><img  class="bd-placeholder-img card-img-top" src="${image}"/><div class="card-body"><h3>${title}</h3><p class="card-text">${description}</p><div class="d-flex justify-content-between align-items-center"><div class="btn-group"><a class="btn btn-sm btn-outline-secondary" href="${url}" target="_blank">View Story from Source</a></div></div></div></div></div>`;contentRow.innerHTML += htmlCard;fetch("http://localhost:3000/contents", {method: "POST",headers: {"Content-Type": "application/json"},body: JSON.stringify({title: title,url: url,image: image,description: description})}).then(resp => resp.json()).then(content => contentCard(content));addContentForm.reset();});};// delete content// Like Imagesconst deleteContent = contentId => {data = {content_id: contentId};return fetch("http://localhost:3000/contents", {method: "DELETE",headers: {Accept: "application/json","Content-Type": "application/json"},body: JSON.stringify(data)}).then(console.log);};formHTML();getContents();addContent();

The full source code can be found at

Thank you

I change the world by helping people to get started in Tech and Social Entrepreneurship.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store