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', '>= 6.0.2.1'
# 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: https://github.com/rails/jbuilder
# 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]
end
group :development do
gem 'listen', '>= 3.0.5', '< 3.2'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
# 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: https://github.com/cyu/rack-corsRails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end

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

Migration

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"
end
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"
end
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
end
add_foreign_key "comments", "contents"
add_foreign_key "comments", "users"
end

Model

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
end
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
end
class Comment < ApplicationRecord
validates :content, presence: true

belongs_to :user
belongs_to :content
end

Above I added validation and relationships between the models.

Controller

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
}
else
render json: {message: "Can't find the content"}
end
end
def create
content = Content.new
content.title = params[:title]
content.url = params[:url]
content.description = params[:description]
content.image = params[:image]
content.user_id = 1
content.save
if content.save
render json: content
else
render json: {message: "Error creating a content"}
end
end
def destroy
content = Content.find(params[:id])
if content
content.destroy
render json: {message: "Deleted succesfully"}
else
render json: {message: "Can't find this content"}
end
endprivatedef content_params
params.permit(:title, :url, :image, :description)
end
end

Comment Controller

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

Our API is ready for testing Using postman starting the server with ‘rails s’ and sending a get request to 127.0.0.1:3000/contents 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
index.html

<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="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" 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="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" 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 https://github.com/peterayeniofficial/js-rails-readit

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