This is meant to give you an understanding of Rails, React, and GraphQL by building a project from the ground up. Given variations in understanding between developers, this guide assumes you know nothing about anything. Feel free to skip what you already know about.
You will need some basic tools to get started. The starting blocks are Ruby, RubyGems, and Rails.
Ruby is the language that Rails and the rest of our backend logic is written in. If
you are using macOS, Ruby should already be installed. Verify this by doing
$ ruby --versionIf you'd like, you can also use rvm to update to a later version.
Rubygems should already be installed if you have a version of Ruby > 1.9.1
You can verify you have it by doing
$ gem --versionNow that you have both Ruby and Rubygems, all you have to do to get Rails is
$ gem install railsYou can make sure it's available by doing rails --version.
This only applies to macOS users!
Go here for instructions on how to install Homebrew (a package manager for macOS).
Using Homebrew, install nodeJS/npm, yarn, and PostgreSQL.
Once you install the latest nodeJS, it should come with npm, but, yetibook will use yarn instead.
$ brew install node
$ brew install yarnPostgreSQL is an open-source, object-relational database.
$ brew install postgresqlOur project will be a Facebook for Yetis, aptly named Yetibook.
Generate a new project in your current directory by doing
$ rails new yetibook -d postgresql --webpack=reactWhat this command does is:
- Create a
newproject withRailsin our current directory calledyetibook. - Specify that the database that
yetibookwill use will bePostgreSQL. - Tells
Webpackerthat we want to useReact.
rails -h and rails [command] -h can give you more information.
At this point, the project should be functional. Try to run
$ rails serveror
$ rails sRails gives plenty of options to generate things to speed up development. cd into your rails project and then do
$ rails generate --helpto see what options you have at your disposal.
Rails has an alias for generate, so you can do
$ rails g --helpif you enjoy brevity.
To see all of the shortcuts try rails --help.
Let's finally generate some components:
$ rails g model yeti name:string email:string:uniq password:digestWe just
- Created a new
modelfor Yetis (aka, users of our application). - Told
railsthat eachyetihas a name, unique email, and a password
$ rails generate controller yetis create- This will create a controller called
YetisControllerwhich will be responsible for handling changes to ouryetimodel. - Make an action called
new; this is aGETrequest for the signup page. - Make an action called
create; this will be aPOSTthat occurs whenever a new yeti joins Yetibook. - Make an action called
index; aGETfor every yeti. - This also creates a view for users of our application to get to add themselves to Yetibook.
- Add a test file for this controller.
You should explore the files that this has created in the app and test directories.
$ rails testor
$ rails tOops! Looks like we got an error when we did that.
$ pg-0.21.0/lib/pg.rb:56:in \`initialize\': could not connect to server: No
such file or directory (PG::ConnectionBad)All this means is that we need to start the PostgreSQL database.
$ pg_ctl init -D path/to/db/files
$ pg_ctl -D path/to/db/files -l logfile startWe're still not done. We need to create test and development databases. It would probably be easier to use SQLite, but, for the purposes of the walkthrough, let's keep going with PostgreSQL.
$ createdb yetibook_test
$ createdb yetibook_development
$ rails db:migrate
$ rails t(The createdb command should've come with PostgreSQL.)
Our tests finally ran--but now there are different errors. Let's solve those and then write a test of our own.
First, uncomment the bcrypt gem in our Gemfile.
Make sure to run a
bundle installto ensure that the gem is installed!
Second, we need to update our fixtures. Fixtures are what Rails uses to populate our test database with data. Just go to test/fixtures/yetis.yml
and make sure that the data have different email addresses to quell the last error.
Now everything should be green!
Let's follow TDD and write a new test of our own.
In test/models/yeti_test.rb:
def setup
@minimum_valid_yeti = Yeti.new(name: 'new yeti',
email: 'foo@example.com',
password: 'abc123',
password_confirmation: 'abc123')
end
test 'new yeti is invalid without an email' do
invalid_member = @minimum_valid_yeti
invalid_member[:email] = nil
assert_not invalid_member.valid?
endThis test should fail with something that looks like this:
Failure:
Expected true to be nil or falseThis failure occurred because our new Yeti was still valid. We don't want them to sign up without an email; how else can we send them spam?
So, following the spirit of TDD, let's update our Yeti model with the following:
app/models/yeti.rb
validates :email,
presence: trueNow do rails t and everything should be green again!
We can put further validations on emails (e.g., has an @ symbol). You should explore this yourself.
We'll use react-rails to fulfill our dependency on React. Add the following to your Gemfile.
# React for front-end
gem 'react-rails', git: 'https://github.com/reactjs/react-rails.git', branch: 'master'bundle install will add the react-rails gem to our project.
We'll also need to do
$ rails g react:installWe added webpacker at the beginning of the walkthrough. Webpacker combines all of our JavaScript components into one file so that it becomes easier to distribute to the end user. To make use of it, we need to copy
<%= javascript_pack_tag 'application' %>
into the header of app/views/layouts/application.html.erb.
Open a new terminal window and enter the following:
$ bin/webpack-dev-serverThis gives us live reloading as we work on our React components.
Let's create a simple component to view all of the Yetis who've signed up.
Create a new directory app/javascript/components.
Create a new file in that directory Table.js.
Add the following to Table.js:
import React, { Component } from 'react';
const Table = () => {
return (
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr key="1">
<td>Ice Berg</td>
<td>ib@example.com</td>
</tr>
<tr key="2">
<td>Glacial Pace</td>
<td>gp@example.com</td>
</tr>
</tbody>
</table>
);
}
export default Table;This table has no useful functionality yet, but we're just trying to get everything connected.
Add
<%= react_component("Table") %>
to app/views/yetis/index.html.erb.
Now you can go to localhost:3000/yetis/index to see our new table!
There has to be a way to get data into our front-end. Static React components make for a pretty dull page, no? We'll use the graphql gem for this. Add the following to your Gemfile.
# Use graphql for API
gem 'graphql', '~> 1.7'Follow this up with another bundle install.
Now, under the app directory, create a folder called graphql.
Under that folder create two directories: mutations and types. We won't mess with mutations for now, but know that this is where they should go.
Create one more thing: inside of the graphql directory, you should create a file called yetibook_scheme.rb. The schema tells GraphQL where to go to find out how to respond to requests. Our file looks like this:
YetibookSchema = GraphQL::Schema.define do
mutation Types::MutationType
query Types::QueryType
# subscription Types::SubscriptionType
endAgain, subscriptions are another GraphQL type that we won't worry about right now.
You'll want to create a new directory called types. Then create types/mutation_type.rb and types/query_type.rb. This will serve as the entry points for queries and mutations. Ignoring mutation_type for now, put this into query_type.rb:
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
description 'The query root for this schema.'
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :yeti do
type types[Types::YetiType]
description 'Get info on yetis.'
argument :name, types.String, 'Get info on yeti with name, if given. Otherwise, get info for every yeti.'
resolve -> (obj, args, ctx) do
args[:name] ? [] << Yeti.find_by(name: args[:name]) : Yeti.all.to_a
end
end
endSo this makes a new query, called yeti, that we'll use to get grab info on the users of Yetibook. Please note that we use types[Types::YetiType] to indicate that this query always returns an array. If we weren't returning multiple objects, we could just do Types:: YetiType.
"But wait," you cry. "There's no YetiType!"
types/yeti_type.rb:
Types::YetiType = GraphQL::ObjectType.define do
name "Yeti"
description 'A Yeti that shares all of its personal information.'
# `!` marks a field as "non-null"
field :id, !types.ID
field :name, !types.String
field :email, types.String
endSo now we need to see what we've done so far. Go to localhost:3000/graphiql (you may need to restart your Rails server).
Create a new Yeti if you haven't already like this:
$ rails c
$ Yeti.new(name: 'Abominable', email: 'snowman@yeti.com', password: 'abc123!@#').save
$ exitCreate as many as you want like this.
Now, here are two queries you can type in (click the big play button to run the query).
{
yeti {
id
name
email
}
}{
yeti(name: "Abominable") {
email
}
}See what we did there? We can change what data we get back from the query so that we only get what we need!
We're so very, very close to actually having everything we need; we just need to be able to see use GraphQL in React.
You're probably thinking, "New library; update the Gemfile."
Not so fast! Try doing
$ yarn add apollo-client apollo-cache-inmemory apollo-link-http react-apollo graphql-tag graphqlto add Apollo.
Create a new file in app/javascript/components called ApolloClient.js. Make sure that file mirrors the following:
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
const client = new ApolloClient({
link: new HttpLink({ uri: 'http://localhost:3000/graphql' }),
cache: new InMemoryCache()
});Create another file: app/javascript/components/TableApp.js:
mport React, { Component } from 'react';
import { ApolloProvider } from 'react-apollo';
import { client } from './ApolloClient';
import TableWithData from './Table.js';
export default class TableApp extends Component {
render() {
return (
<ApolloProvider client={client}>
<TableWithData />
</ApolloProvider>
);
}
}Update app/views/yetisindex.html.erb to:
<%= react_component("TableApp") %>Then, finally we need to update Table.js to use Apollo.
You should do this yourself, but here's a hint:
import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
export const Table = ({ data: {loading, error, yetis} }) => {...}
let YetiQuery = gql`query { ... }`;
const TableWithData = graphql(YetiQuery)(Table);
export default TableWithData;Good luck!
Phew! We've done a lot to help every yeti on the planet feel connected.
Please note for the following glossaries and links that they are included in the order in which they are written.
Version of Ruby on your path
ruby -vor
ruby --versionGenerating a new Rails project
rails new [project name]See rails new --help for more detail.
Starting the PostgreSQL server
pg_ctl -D /path/to/db/files -l logfile start(Usually, your path starts with db/)
Presented in the order they are mentioned.
-
Ruby: https://ruby-doc.org/
-
rvm: https://rvm.io/
-
Rubygems: http://guides.rubygems.org/
-
Rails: http://api.rubyonrails.org/
-
Yarn: https://yarnpkg.com/en/
-
PostgreSQL: https://www.postgresql.org/docs/
-
React: https://reactjs.org/docs
-
GraphQL: http://graphql.org/learn/