Ruby on Rails is a web framework that contains many libraries you’d need to create and deploy a successful web application. We often take for granted the ability to run
rails new to create a fully functional web application with tons of built-in features. For many of us, that’s good enough since the goal of Rails is to magically have everything work without needing to know what’s happening under the hood.
In order to fully appreciate Ruby on Rails in all its glory, we’ll first need to understand the problem it’s trying to solve. Let’s picture a world where Rails doesn’t exist. It’s hard, I know.
How would we build a web application only using the standard Ruby libraries? In this article, I’ll break down the key foundational concepts of how a web application works while building one from the ground up. If we can build a web application only using Ruby libraries, why would we need web server interfaces like Rack and web applications like Ruby on Rails? By the end of this article, you’ll gain a new appreciation for Rails and its magic.
I’ll cover topics including: network protocols (TCP and HTTP), persistent data stores, web server interface (Rack), and Rails libraries (Action Controller, Action Dispatch, Active Record, and Action View). In the first half of the tutorial, we’ll only be using core Ruby libraries, which is sufficient to learn the key concepts of how a web application works. In the second half, we’ll substitute the code with modules from Rails.
Before We Get Started
My assumption writing is you, dear reader, know how to use a file editor and navigate the terminal. You should also be familiar with terms related to the Internet, like web browsers. Being familiar with Ruby or Ruby on Rails and data structures isn’t a requirement, but it’ll help greatly! Lastly, it’s assumed you’re following along on a computer with MacOS and know how to install and use Ruby gems.
In any case, I highly recommend following along with the code samples and running the code on your computer to get the full learning experience. After all, there’s no learning without experience doing.
The Mirth Web Application
The web application we’ll be building today is called Mirth, a web application with the functionality for users to see displayed data and enter new data that would persist in the application. How you want the data to look is up to you. The Mirth web application for this tutorial will be a birthday tracker. Instead of relying on a certain social media to inform us of people’s birthdays, you can now ask them, record it, and view it in a list.
Conversations with the Network
Now that we got everything in order. Let’s start with the basics of how a web application works. How does the web application, or server, talk to the internet? Like humans, networked applications have a specific protocol they use to communicate with each other, called the Transmission Control Protocol (TCP). You may have already heard of TCP before but what’s the role of TCP in web applications?
To understand what TCP does, we must first understand the Internet Protocol (IP). You'll soon find that levels of abstraction is a theme to understanding how this works. TCP is simply an abstraction of IP, a lower level protocol that’s used to transfer individual packets of data.
Although IP is the basis of how the Internet works, it’s a primitive protocol and therefore unreliable. Using IP poses risks like the wrong order of packet delivery or no delivery at all. TCP is built on top of IP to make the system more reliable with additional features. To solve the issue of out-of-order packet delivery, TCP allows the sender to add an auto-incrementing number inside the packet that is reassembled at the receiver’s end. Another key feature TCP adds is the concept of sockets.
Sockets are meant to represent a persistent connection between a server and a client. Ruby has a core library called
sockets that builds on top of the TCP implementation from your operating system (which by the way, is yet another abstraction!).
Use Ruby’s Socket Library
socket library allows you to create a TCP server socket. In our case, Mirth’s server socket wants to keep an eye out to any incoming connections. But, how would a connection know the application to connect with? TCP creates a socket on a specific port that can be identified as the application that’s using it. In this case, we specify Mirth’s port to be 1337.
Accept Incoming Connections
Now that the server socket is created, we decide what we want to give the client when a connection is made. In a loop, we accept any clients through
#accept. Once a client is accepted as a connection, we receive information in a form of an IO object.
Get the Input from Client Side
Once you’ve accepted the client’s connection, now you can get any input from the client. In this case, we ask the client “What’s your name?”, and retrieve and regurgitate a modified reply.
Close the Client Socket
After we’re done responding to the client, we must close the connection.
Run the Code to Create a Server Socket and Client
View the complete code snippet,
mirth-1.rb on Github.
- To run this code on your computer, run the file,
ruby mirth-1.rb. By doing so, you create a server socket. Now you need a client to connect to the server with.
- Open a new terminal window and run
nc localhost 1337. You’re now connected to your local server socket at port number 1337 using netcat. Subsequently, entering any input after the connection is established results in the print statements we have in the code.
More Structured Conversations with the Network
Now that you’re able to create a TCP socket, you’re ready to go further up the abstraction. Currently our TCP server still can’t communicate properly with a browser. That’s because the web browser speaks in the language of another protocol called the Hypertext Transfer Protocol (HTTP). TCP is application agnostic so any type of application can use TCP. HTTP, on the other hand, is specific to the web. So, we can actually implement HTTP in Ruby ourselves.
There are many types of browsers and servers out in the world and that’s why we have HTTP. The protocol was set up by the HTTP Working Group so everyone in the world can speak in the same language when it comes to web browsers.
Every single HTTP message has the same structure:
- a start line
- zero or more header fields
- a blank line
- an optional message body.
HTTP messages specialize into two types: requests and responses. A web browser can make a request to a server for a page, and the server obliges with the requested page in it’s response. The difference between requests and responses differ only in the
start-line of a HTTP message, where a request has a
request-line and a response has a
status-line. The structure of these requests and responses are specified in detail in a document called RFC.
Let’s break this part down into requests and responses.
From the perspective of the Mirth web application, it wants to be ready to serve the client with the requested information after the connection. Instead of providing a string of your name from the previous example, the client will now make the HTTP request.
The key component of a HTTP request is the request-line that contains vital information for the web application to act on. These are the method token, target path and HTTP protocol version number. The method token is the type of request from the server, the two common ones being GET (to get data) and POST (to post data to make a change in the server). As you would expect, the method token leads the code down separate paths depending on what’s requested. The target is the path the request is coming from. For example, the target path the server gets from a user requesting info to this webpage,
baked-brownies/12. Lastly, the version number is the HTTP version the web browser is using, the most common one being
Get the Request-line of the Request
Once the server accepts the client socket, we read the request line of a request. Unlike the #
gets method that we’ve used previously,
#readline returns an error if there’s no more input to get.
Break Down the Request-line of the Request
We break down the HTTP request into parts, the
method_token, target and
version_number. All these parts are essential information to a server on how to respond to the request. The code simply prints the information we got from the HTTP request and returns it in the body of the HTTP request to display on the web browser page.
Run the Code to Make a Request
View the complete code snippet,
mirth-2.rb on GitHub.
- For this exercise, run
- Instead of making a request from another socket, we make a request directly from the browser. Open your browser and enter
localhost:1337/cakes-and/pies. Your browser won’t have anything displayed because the server hasn't responded. Instead, make note of the string logged in your output.
Responding to Requests
The key part in our next code snippet is the
http_response variable that contains:
- the start-line
- header fields
- a blank space
- the body, containing the message to be rendered by the browser.
The first part of the HTTP response is the status code that represents the start-line. In our case, we want to ensure the browser that all is well, so we make the status code
201 OK. If there are any issues with the server, the browser would then be able to relay that information to the users, which explains the most commonly seen
404 Error status code when you visit a bad webpage.
There are other notable parts of the HTTP response like the header fields. The header fields are lines of key-value pairs, in our case,
text/html. You can find a list of other headers at the RFC documentation page.
The types of header fields may vary between request and responses, but for responses, the header is where you enter any additional information about the response like path of redirection, caching and cookies, and security-related details. The browser reads the header fields and takes action accordingly, like redirect to the appropriate page, store the appropriate user settings, or secure the webpage.
The body is where you put the HTML code that’s rendered by the web browser.
Adding Complexity to the Web Application
Alright, let’s add some complexity to our web application and reply to the web browser with better-formed HTTP responses of our own. We’ll add some mock data to display (in this case birthdates), and two HTTP responses to display data and update the data with new user input.
Add Some Mock Birthdates
Let’s first create some default data. All the birthday data in a hash that’s found right after the server is initialized. Note that the data isn’t persistent, meaning once the server restarts, any objects added to this hash disappears.
Create a Case Statement for all the Endpoints
Next, we create a case statement in order to separate the different method tokens and path targets. This gives us a clear outline of the three endpoints we have for our little web application. The three endpoints are,
/show/birthday, POST /add/birthday and any other generic path. If the request matches any of the endpoints, the code within the block will run.
Add the GET Endpoint
Because each endpoint will yield a different HTTP response, we define the following variables,
response_status_code, content_type and
response_message for each of the endpoints.
Add the POST Method
As for the POST method, we add some user input to our birthday data hash. For now, it won’t be persistent, but you’re still able to add to the list while the same server is running.
The POST endpoint will look a little different from the GET endpoint. The status code won’t be
“200 OK” this time because instead of just informing the browser that the request has succeeded, we can be more helpful and redirect the user to the
/show/birthdates page using a
“303 See Other” status code.
Next, there’s an important header field needed from the request, the
Content-Length header. The content length header is the size of the body message in the HTTP request in bytes. Once we know how many bytes the message body consists of, we accurately read the body from the client socket’s IO. Lastly, we use Ruby’s built in decoder library called uri to decode the body of the request into a Ruby hash that we append to the end of our list of birthdays.
Construct the HTTP Response
Once we’ve successfully determined all the components of the HTTP response based on each of the end points, we reconstruct the response. The response is constructed following HTTP specification: the response start-line contains the version number and response status code, followed by headers, an empty line, and the response body. In the next exercise we accept GET and POST responses from the web browser.
Run the Code to Accept GET and POST Responses
View the complete code snippet,
mirth-3.rb on GitHub.
- Let’s run the code,
- Similar to the previous exercise, we open a browser to the URL, but this time we go to
localhost:1337/show/birthdaysthat’s the URL to show the birthdates.
- Add a new birthday information in the form, the page updates with the new data.
- Restart the server by pressing Control-C to close the server and running it with
ruby mirth-3.rbagain. Restarting causes the server to start with the default data provided in the code.
Let’s try to solve the data persistent problem in the next section.
Persistent Data Storage
What is a web application without the ability to store data? We easily collect and display some user-submitted data but it won’t stick around once the socket is closed. We need a way to store the data so the next time the server restarts, we can still use the same set of data that was previously saved. But how would we?
Obviously the answer is to store in a database. A database stores information on physical disks or servers. But pretend we live in an age where databases didn’t exist. What’s the most primitive way to persist some information? We save our data to a file.
We could use a plain text file, but we would have to implement our own format for representing the data in plain text. The good news is that Ruby has a built-in library, PStore, that translates Ruby objects into binary and stores it in a file. Alternatively, PStore is also able to read from the file and translate the binary back into Ruby objects. The serialization and deserialization is the process of marshaling Ruby objects into binary and vice versa.
However, PStore stores the file in binary that’s not readable by us humans. So, we move up the abstraction and use
YAML::Store, which is an implementation of PStore. Instead of serializing the Ruby objects into binary,
YAML::Store serializes into the human readable YAML file format.
What the structure of a YAML file looks like.
Run the Code to Create Persistent Data Storage with YAML
View the complete code snippet,
mirth-4.rb on GitHub.
- Ensure you have both
mirth-4.rband the YAML file,
mirth.ymlin the same folder.
ruby mirth-4.rband go to
localhost:1337/show/birthdayson your browser. You’ll see the default data displayed exactly the same as the previous run.
- Input some data and restart the application. You should still see your previously inputted data! The YAML file will be updated to reflect the new input.
Breakpoint, You’ve Got a Working Web Application Now!
Phew, that’s a lot to take in. If there’s a topic you’re interested in learning more about see the Additional Information section at the end of the post.
But wait—we have a working web application written fully only with Ruby libraries. It took a lot for us to get here. We needed to understand all the low level details of how TCP runs, what makes HTTP requests and responses, and how to persist data by storing it in a file. I mean… surely there’s a better way to write a Ruby web application but how? Yep—with more abstractions!
Rack (n’ Roll) Specification
The code that implements the TCP sockets to create a network connection is generally similar throughout all the types of web applications—the server creates a socket to accept clients. HTTP requests and responses are also standardised across all types of web applications. As we’ve seen above, it takes a lot of time and effort to rewrite the functionality to parse HTTP. A web developer would have to understand intricate details about the network protocols.
There’s an opportunity to abstract these functionality away from the developer who’s just trying to make a web application, reducing the overhead work. Instead of creating a network connection and writing HTTP parsing logic every single time we want to start a new web application, we can use an application server.
An application server is a program that deals with HTTP on behalf of the web application, it accepts and parses requests, and then generates and sends responses. Just like how HTTP has its specifications, application servers too have an agreement on how the server talks to our code.Rack is the specification for Ruby application server. If our web application implements the Rack specification, we can then run any application server that supports the Rack specification. There are many application servers you can use based on your web application’s needs, and we’ll be using
pumaas our application server in this tutorial.
A Rack application (or a web application that follows Rack specification) is any Ruby object that responds to the #call message, accepts a hash argument called the environment, and returns a three-element array that contains:
- a status code
- hash of response headers
- an array of the response body.
It’s similar to how a HTTP message would look like, but instead of a string format, they are Ruby objects.
Imagine your web application as a box that takes in an HTTP request and outputs a HTTP response. Rack is a layer on top of the web application that enables Puma, the application server, to intercept the HTTP request on the web application’s behalf and return the three-element array.
Once you’ve made your application a Rack application, you can access more functionalities provided by Rack. But first, let’s see how a Rack application looks integrated into our existing application.
Create an Application Server and New
Instead of instantiating our own TCP socket, we defer it to Puma by following the Rack specification. The first thing our app does is create a
rack::request object with the environment provided. After this, we won’t have to worry about accepting and splitting the HTTP request’s request-line ourselves because it’s provided within the request object.
This code was previously our case statement that determined which endpoint to lead the request down. Now we use built-in request methods like
Another notable built-in method is the
#params method, which returns the request’s body and query string decoded into a hash of parameters. We use this in the POST endpoint where we take the parameters from the request, which is the user’s input, and store it in our YAML file.
Return the Three-Element Array
As outlined in the Rack specification, we need to return a three-element array that consists of the status, headers, and body. Note that the status has to be an integer 200 or larger, the headers has to be a Hash and the body has to be an object that responds to
#each. To make things simple the body object is a Ruby Array in this example.
Run the Application Using Puma
Lastly we want to make sure to run the application using
Rack::Handler::Puma, which is the Puma API to run a Rack application. Puma goes on to create a TCP socket and handles the networking on the web application’s behalf.
Run the Code to Integrate
Rack::Request in Our Example
View the complete code snippet,
mirth-5.rb on GitHub.
ruby mirth-5.rb. Mirth should have the same behaviour as the previous run. Notice how much detail has been removed and replaced with methods from the
rack::requestlibrary in the code.
Now that Mirth is a Rack application using
rack::request, we can now use
rack::response to wrap the HTTP response objects. Similar to how
rack::request worked, we are encapsulating the response logic of the application using
rack::response object. Once the HTTP response object is created and ready to go, Rack will send the response for the application.
Create a new Rack Response Object
Similar to how a
rack::request object is created, we create a new
rack::response object to handle the response.
Use the Built-in
Again, similar to how you can ask a
rack::request questions about the request, you can now modify the response object to your liking based on the different endpoints. For example, in the GET endpoint, we will use
#write to append a string to the body of the response. This will now be done behind the scenes in the
rack::response also takes care of setting the defaults. You won’t have to declare the status as 200 for each endpoint as it’s assumed that 200 is the default code. The content type is set to “text/plain”. If you’d like your response to look differently, you can modify it.
For page redirections, you can now use r
#write method to set the status of redirection and the path to be redirected to. What was previously done in two lines of code is now done in one.
Mark the Response as Finish
Once every single endpoint is done modifying the response object, you can call
#finish in order to send the response through Puma.
Run the Code to Integrate
Rack::Response in Our Example
View the complete code snippet,
mirth-6.rb on GitHub.
ruby mirth-6.rb. Notice how different the code looks—most of the manual variable assignment to ensure the proper HTTP response is now gone and taken care of by r
Persistent Data Storage, For Real
Clearly, storing our data in a YAML file isn’t scalable with more requests. Plus, once your application has to store more complex data models and it will, YAML file storage becomes problematic. Millions of data items take too much time and memory to be processed and loaded on every single request. In yet another abstraction, we use a database so we won’t have to worry about optimising the queries and updates we make.
SQLite is a lightweight relational database that doesn’t use the traditional networked client-server model of alternatives like MySQL and PostgreSQL. Instead, the SQLite database engine runs in the application process, and the data lives in a local file that’s read directly by the web application. SQLite is great for what we’re trying to accomplish because it means we don’t have to deal with the additional complexity of running a separate database server.
Unsurprisingly, there’s a Ruby gem called
sqlite3 whose API we conveniently use. We now replace the
YAML::Store read and write actions with the SQLite database read and write actions through the gem’s API.
Create a SQLite3 Table Called Birthdays
The first step is for us to create a SQLite3 table with the appropriate columns. For us, it’ll be a name column and a birthdate column. To keep things simple both columns are the String type and we insert some default values like the previous Mirth implementation.
Create a SQLite3 Database Object
After creating the table, we create a reference to it in the code through the
sqlite3 gem. We access the database table by creating a new instance of
SQLite3::Database.new with the table’s name. Thanks to SQLite3, we also add an optional setting to return all results in the form of a Ruby hash using the
results_as_hash: keyword argument.
Get All Birthday Data
When we were using YAML::Store, we created a transaction in order to obtain data from the YAML file. Using the SQLite3 gem, we execute a query directly on the database table. For our GET endpoint, where we want to display all the birthday information stored in the database, we execute
SELECT * FROM
birthdays which returns all the birthdays in a Ruby hash.
* is a risky query to have because of the performance implications, but for our purposes it’ works fine. At this point, we access the Hash just as we would using YAML::Store.
Create a Query For New User-Inputted Row
As for our POST request endpoint that creates a new birthday data, we execute an INSERT query,
INSERT INTO birthdays (name, date)VALUES(?, ?) with the new name and birthdate. SQLite3 handles the addition of the new row into the table.
Run the Code to Create Persistent Data Storage with SQLite3
View the complete code snippet,
mirth-7.rb on GitHub.
- Before running the ruby file, we must create a database and table. Run the following on the terminal to create the database and table:
CREATE TABLE birthdays (name TEXT, date TEXT);
INSERT INTO birthdays VALUES("Gma", "2021-01-01");
INSERT INTO birthdays VALUES("Tom", "2021-01-02");
INSERT INTO birthdays VALUES("Sesame", "2021-01-03");
ruby mirth-7.rb. You’re now running Mirth with a database and all reads and writes will be through Sqlite3.
In this part of the tutorial, we substitute our work with some Rails libraries without actually requiring Rails as a whole. This will give you a better understanding of what each library can do individually to paint a better mental model of what Rails is capable of as a whole.
Before we begin, I’d like you to reflect on if there are any parts of our Mirth application that could be abstracted away, meaning which common parts of the application can be removed and replaced just with an API.
Using the Active Record Library
Active Record, one of the main components of Rails, is an object-relational mapping library, meaning it maps Ruby objects to rows in the database. It may sound obvious to you that a
House object should map to a row in the
house table. But this is an implementation of the active-record pattern, where “an object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.” Using Active Record library, we interact directly with the database through object-oriented API without having to concern ourselves with the SQL database.
Active Record specialises in Create, Read, Update, Delete (CRUD) operations. The library takes care of the complexity of performing these CRUD operations manually. For example, the object, Birthday, is mapped to the Birthdays table in SQLite. Note that the Active Record model’s name is singular while the table’s name is pluralized.
For example, in our GET request endpoint, we can use
Birthday.all to get a list of all the rows in the birthdays table. The returned object is an array of Birthday objects that has helper methods for each of the columns like name and birthday.
As for our POST request endpoint, we use Active Record by simply using
Birthday.create(name:, date:) to create the new user inputted row. Active Record does the work of creating and saving the new record in the database table.
Before we use these built-in methods from the Active Record library, we must establish a connection between Active Record and the Sqlite3 database, and then create a class connecting the data model and the table in the database. The Active Record library handles the matches of the class name with the table name in the database.
Using the Action Dispatch Library
The endpoint routing logic we see in our code is the other thing we can abstract away. In this case, Action Dispatch is the library we use. More specifically,
ActionDispatch::Routing::RouteSet provides a router that can process incoming requests and routes them to the appropriate code path.
Currently the code still handles requests by checking for each request method and path to decide which code path to take. Using Action Dispatch, we break up our application into multiple mini Rack applications and configure a route for each endpoint separately. The
RouteSet#draw method is used to take a block with several helper methods in the form of the request method (i.e. GET, POST) to configure the request to the particular endpoint.
By now we have some mini Rack applications in the form of an endpoint leading up to a larger Rack application. Any incoming requests goes through the large Rack application, that’s led by Action Dispatch down to the corresponding endpoint.
However, it seems like we can do even better than having three mini Rack applications. Let’s see what Action Controller has to offer.
Using the Action Controller Library
Action Controller is another library provided with Rails. It follows the MVC model where given a request, the controller will determine what to do with it like figuring out what data to query and which view to render. As you see in the MVC diagram, the controller acts as the intermediary entity between the view and data model.
In Action Controller, the methods in the
ActionController::Base class are called “actions”. First we create a controller for our Active Record model, calling it
BirthdaysController. (Note the pluralized version of the Active Record model, Birthday.)
In our case, the action could be called
#show_all_birthdays because that’s exactly what our action would be doing—routing the GET endpoint to display everyone’s birthdays. These actions essentially are the endpoints that were previously in a mini Rail app.
However, because we want our action to work along with Action View (as you’ll see in the next section), we’d have to follow the Rails conventions of naming and call the action
GET /birthdays and
POST /birthdays. Rails has several other naming conventions that can be found in the Rails Routing API.
Using Action Controller and Action Dispatch, we route the requests onto the ActionController’s action. Action Controller makes accessing the request and generating response much more convenient through instance methods such as,
#redirect_to. All these methods that were previously provided to us by Rack Request and Rack Response libraries are now handled by Action Controller. As a result, the code for each HTTP endpoint is much shorter.
Aside from controlling the flow of request through the application, Action Controller has many, many other functionalities, such as HTTP authentication, storing data in sessions and raising exceptions throughout the application’s lifecycle.
Using the Action View Library
In the section about Action Controller, you’ve probably noticed that a huge chunk of HTML code that was previously rendered inline is missing. Yet, the web application still works! Action View, the view component of the MVC model, works with Action Controller to help us generate HTML code from embedded Ruby (ERB) code through ERB templates. The HTML code is then passed back to the Action Controller and placed into a response object that’s returned to the client.
The caveat here is that the view has to be stored in a way that matches Rails conventions. In our case, we declare the directory
birthdays/index.html.erb, and ensure that the controller accesses to the view path by using
Within the HTML-ERB file itself, there’s some functionality that is abstracted away, most of them being boilerplate code like the form and content tag. The Action View library provides us access to helper methods that replace the boilerplate code. Additionally, any instance variables from the controller are accessed in the view using the same name. In our case, after the controller action queries all the birthdays, we read the instance variable
index.html.erb and display them.
Run the Application Using Rails Libraries
View the complete code snippet,
mirth-final.rb on GitHub.
Alright, now that you have a general idea of what to expect, check out the finalized Mirth web application that uses Rails libraries.
- Assuming you’ve already created an SQLite3 database from the previous section, place
index.html.erbin a folder called
- Go to the URL
localhost:1337/birthdaysto see the same web application running but now, using Rails libraries.
The End of the Beginning
Rails does so much more than what we just covered in this tutorial and each section can be written in much more detail. On top of the primary libraries we learned about in this tutorial, Rails also has libraries for sending emails, creating and managing background jobs, and adding internationalization to your web application.
With just a little bit of familiarity with the Rails framework, you’re able to build a web application—that’s the magic of Rails. Building your own Ruby web application from scratch however isn’t only educational—I’d argue that it’s also a rite of passage in a Ruby and Rails developer’s career!
Just kidding. This was fun, but it’s probably best if you used Rails for your web development needs. Now that you understand the low-level details of how a web application works in Ruby, you’re able to appreciate why Rails tries to hide all the implementation details under the hood.
Maple is yet another software developer who made it without a formal education in CS. She currently works on the TruffleRuby team at Shopify where speedy Ruby code isn’t a fantasy. Previously, Maple worked on Packwerk, a Ruby gem to enforce modularity in Rails applications. Reach out to Maple on Twitter to rave about well-documented open source projects and cute pets pictures.
- Ruby Gems Basics
- Transmission Control Protocol (TCP)
- Internet Protocol (IP)
- Class: Socket (Ruby 2.7.0)
- HTTP Working Group
- Module: URI — Documentation for uri (2.7.0)
- rack/rack: A modular Ruby web server interface
- puma/puma: A Ruby/Rack web server built for concurrency
- Rack specification
- Rack::Request object
- Rack::Response object
- Active Record
- Active record pattern
- Class: ActionDispatch::Routing::RouteSet — Documentation for rails (22.214.171.124)
- Action Controller Overview
- Rails Routing API
- Method: ActionView::ViewPaths::ClassMethods#prepend_view_path
- Ruby on Rails 6.1.3 Module ActionView::Helpers::TagHelper
- We're planning to DOUBLE our engineering team in 2021 by hiring 2,021 new technical roles (see what we did there?). Our platform handled record-breaking sales over BFCM and commerce isn't slowing down.