The idea for AnthologyApp came from my own creative process as a musician and artist. I have been trying to get into different forms of creativity and I have recently tried to overcome my fear of writing. I hate writing and I don't feel like a very good writer. When confiding in a writer friend of mine about this issue he suggested I try short stories and so I started writing down every idea I had for a story in my phone notes. I thought it would be a great idea to make an app where users can compile their short story ideas, and see other people's short story ideas as well.
AntholgyApp is a place where users can see a list of short story prompts collected from all of the users of the app. Users can browse these, and can write their own short story as a response to a story prompt, and see other people's responses to that prompt too. Users can also create their own short story prompts to share with the world!
To view a blog post I wrote about the app please follow this link:
https://medium.com/@emclawson1/all-about-mvc-model-view-controller-9c87d88b64e0
To view a video walkthough of the app, please follow this link:
A special thank you to Matteo Piccini and Flatiron School for teaching me to code. Thank you to Angel Robiou for helping me overcome my anxiety around writing, and for inspiring me with the idea for this app, and thank you to friends and family that provided short story ideas, and who support me every day.
Sign in page courtesy of Mui React templates and can be found here: https://github.com/mui/material-ui/tree/v5.7.0/docs/data/material/getting-started/templates/sign-in
Additional information about Material UI, including installation, can be found on the website here: https://mui.com/material-ui/getting-started/installation/
This app uses a Rails API and React frontend that can be deployed to a single
domain. For ease of deployment, both projects are contained in the same
repository. All React code is in the /client
directory during development.
When the application is deployed, the production version of the React application
will be generated on the server and placed in the public
directory of the Rails
application, where we can use Rails to serve it.
To run the app locally, install the Rails and React dependencies and set up the database:
bundle install
rails db:create db:migrate db:seed
npm install --prefix client
Install Heroku CLI (if you don't already have it):
brew tap heroku/brew && brew install heroku
Configuration for running in development is in the Procfile.dev
file. Run this
command to start the frontend and backend servers:
heroku local -f Procfile.dev
In development, requests from the React app are proxied, so you can write something like this (without using a domain):
fetch("/me").then((r) => r.json());
Since our deployed app will run on the same domain, this is a good way to simulate a similar environment in development.
Login to Heroku:
heroku login
Create new Heroku app:
heroku create
Add buildpacks for Heroku to run:
- Rails app on Ruby
- React app on Node
heroku buildpacks:add heroku/nodejs --index 1
heroku buildpacks:add heroku/ruby --index 2
Deploy:
git push heroku main
There are a few areas of the code that differ from the typical Rails API setup that merit explanation.
By default, when generating a new Rails app in API mode, the middleware for
cookies and sessions isn't included. We can add it back in (and specify the
SameSite
policy for our cookies for protection):
# config/application.rb
# Adding back cookies and session middleware
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
# Use SameSite=Strict for all cookies to help protect against CSRF
config.action_dispatch.cookies_same_site_protection = :strict
We also need to include helpers for sessions/cookies in our controllers:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include ActionController::Cookies
end
Now, we can set a session cookie for users when they log in:
def create
user = User.find_by(username: params[:username]).authenticate(params[:password])
session[:user_id] = user.id
render json: user
end
We'll deploy our frontend and backend to Heroku on one single app. There are a
couple key pieces to this configuration. First, the Procfile
:
web: bundle exec rails s
release: bin/rake db:migrate
This gives Heroku instructions on commands to run on release (run our migrations), and web (run rails server).
Second, the package.json
file in the root directory (not the one in the
client directory):
{
"name": "phase-4-deploying-app-demo",
"description": "Build scripts for Heroku",
"engines": {
"node": "16.x"
},
"scripts": {
"build": "npm install --prefix client && npm run build --prefix client",
"clean": "rm -rf public",
"deploy": "cp -a client/build/. public/",
"heroku-postbuild": "npm run clean && npm run build && npm run deploy"
}
}
The heroku-postbuild
script will run when our app has
been deployed. This will build the production version of our React app. It does
the following:
- removes any old versions of the React code by deleting the
public
directory - installs the frontend dependencies with
npm install
- builds a production version of the React application with
npm build
in theclient/build
directory - copies the built version of the React code into the
public
directory
When a request comes to our server, we can decide if it's request for an API
resource, or a request to view the React application. If it's an API request, we
can send back JSON data from the controller. Otherwise, we can send back the
index.html
file from our public
directory and run the React application.
For our deployed app, we need non-API requests to pass through to our React application. Otherwise, routes that would normally be handled by React Router will be handled by Rails instead.
Setup routes fallback (make sure this is the last route defined in the
routes.rb
file):
# config/routes.rb
get '*path', to: "fallback#index", constraints: ->(req) { !req.xhr? && req.format.html? }
Add controller action:
class FallbackController < ActionController::Base
def index
render file: 'public/index.html'
end
end
Note: this controller must inherit from ActionController::Base
instead of
ApplicationController
, since ApplicationController
inherits from
ActionController::API
. API controllers can't render HTML. Plus, we don't need
any of the auth logic in this controller.