Talks

Building a Rails Controller From Scratch

Building a Rails Controller From Scratch

by Alex Kitchens

In the talk "Building a Rails Controller From Scratch" at RailsConf 2020 CE, Alex Kitchens demonstrates how to create a functioning Rails controller without inheriting from ActionController. The presentation walks through replacing ActionController with the speaker's own implementations, dubbed AwesomeControllerBase and AwesomeControllerAPI. The goal is to uncover the internal workings of a controller by mimicking the core functionalities of ActionController, providing a clearer understanding of how Rails processes requests and responses.

Key Points:

  • Purpose of the Talk: Alex emphasizes his interest in understanding how controllers work in Rails, beyond just writing actions, by building a controller from scratch.
  • Initial Setup: The project starts with a simple Rails blog with two controllers for posts—one for web and the other for API—with tests confirming their correct behavior.
  • Implementing Dispatch Logic: The primary method needed is dispatch, which the router calls to provide the action name, request object, and a response object. Alex details how to correctly handle this method in order to facilitate the flow from request to response.
  • Handling Parameters: To manage request parameters, Alex introduces a modular approach for implementing necessary functionalities such as params, require, and permit, ensuring that the controller can access and process the parameters correctly.
  • Rendering Responses: The presentation also covers the implementation of the render method for both API and web responses. The API version is comparatively straightforward, while the web controller requires implicit rendering to handle HTML templates.
  • Controller Callbacks: Alex explains the importance of implementing controller callbacks (before, after, and around actions) to handle operations that should occur in relation to specific actions, enhancing the controller's capabilities.
  • Conclusion: By the end of the talk, Alex summarizes the outcomes of creating these minimal controller classes, underlining the importance of understanding the interaction between requests and responses in the Rails lifecycle. He emphasizes that while the resultant code is not production-ready, it serves as a great educational tool to grasp the concepts behind controllers.

Takeaways:

  • Building basic components of a Rails controller from scratch allows for a deeper understanding of how Rails operates under the hood.
  • Implementing essential features such as dispatch, rendering, and parameter handling can significantly demystify Rails' controller functionality.
  • Encouragement to explore the ActionController code in Rails for further learning opportunities.
00:00:09.000 Hey, thanks for checking out my talk. I'm Alex Kitchens, recording from home.
00:00:11.400 Today, we're going to build a Rails controller from scratch. We'll go about it this way: I have a really simple Rails blog with a straightforward model. It has two basic controllers, both for posts. The first one is a web version, and the second is an API version.
00:00:17.369 Well, I have a test suite to ensure that they're working correctly. This is what we're going to do: we're going to replace the inheritance of our controllers from ActionController to inheriting from an implementation of our own.
00:00:24.150 Then we'll see what we have to build in place of ActionController to get the app working correctly again. We'll create two controller-based classes: AwesomeControllerBase, which is a replacement for ActionController::Base that will respond to web-specific requests, and AwesomeControllerAPI, which is a replacement for ActionController::API.
00:00:36.120 If you've never used ActionController::API, it is a stripped-down version of a controller class that Rails provides to respond to non-web requests like JSON. At the end of this talk, I'll share a link where you can see the code I've written and play around with it alongside the real ActionController.
00:00:53.649 The code written in this talk is not meant to reinvent the wheel; it will sort of mimic the code of ActionController. My intention with this is to give you insight into the real code of ActionController while taking the liberty to simplify it for learning purposes.
00:01:05.369 In the end, we'll have two controller classes that collectively handle four things, or four modules. In comparison, ActionController::API has 15 modules, and ActionController::Base has a total of 35 modules. So clearly, this is a very minimal happy version of those classes.
00:01:13.350 By building something like this, it will help us understand the inner workings of ActionController. This talk was originally driven by the realization that I don't know a lot about what a controller actually does.
00:01:28.300 I got the basic concepts of the Rails request-response process, but that was pretty minimal. This is what I knew: when a request comes in, at some point it hits the controller, and then it responds with an output like that.
00:01:40.000 I realized that I knew very little about how our controller receives a request or how it processes that request to create a response. Most of my time with controllers has been spent writing actions. Thanks to the conventions and hard work of ActionController, the controller has always been a really easy part of Rails apps. However, my lack of knowledge and my general curiosity inspired this experiment.
00:02:03.060 So here we are: we have our new inheritance and our empty base classes ready. And we have a whole load of test failures. This is where I started my journey. What do we have to do to get this test suite back to green? There are a lot of test failures, but the most common one is an undefined method error called `dispatch` for both controller classes.
00:02:18.140 It turns out this issue arises when the router sends the request to the controller. The router provides the request to the controller and expects the details of the response in return. It does this by calling `dispatch` on the controller class. However, our controller doesn't have this method yet.
00:02:40.640 Here's the actual line of code in the router where the failure occurs. When the router calls `dispatch` on the controller, it provides it with three things. The first is the action that the controller should call, which is in the form of a string, for instance, 'show'. The second is a request object, which is essentially a wrapper around the details of the request.
00:02:58.020 The details of the request live in a hash that looks like this, giving you details on parameters, path info, and other data provided by the original request. The last piece is an empty response object that makes it easy to get and set data for the response. The router provides all this information, and in return, it needs to know three things from the `dispatch` method: the status code, headers, and the body of the response.
00:03:18.470 This information will come from the response object that the router gave us, and this response object comes with some defaults for each of these—most notably, a 200 status code. The response has a handy way of retrieving that data. If I call the `to_a` method on the response, it will give me the details I need.
00:03:35.700 Knowing that this is the point of failure, we can start building our implementation. I've defined a class method called `dispatch`, taking in the arguments from the router, and I use the response to send back the status code, headers, and body of the response.
00:03:49.400 This supplies us with the start of our implementation for dispatching the request of the controller and returning the response. Now, because this method is needed for both the API and base controller classes, I've implemented it instead in a superclass for them to inherit from, which I’ve named SuperBase.
00:04:02.800 With the base classes inheriting from SuperBase, they will share this implementation, and this code fixes the issue of dispatch having an undefined method. We can rerun the tests, which will continue to fail, but for many other reasons. We'll start with this easy one: a failure due to an empty response body from our API.
00:04:10.000 The response is empty because we're providing the default empty body of the response object, which is good. This means that our method is being called and our empty responses are returning their data. We still have not called our action, so there’s nothing to process or render; that's our next step.
00:04:37.470 We have our action method name as a string provided by the router, and we need to call that from here. There's a little problem though. Our dispatch method is at the class level of the controller, but the show method in our controller is an instance method.
00:04:50.430 A simple way to fix this is to move the main dispatch functionality to an instance dispatch method and delegate to it through a new controller instance in the class method. This was an important realization for me because at one point I had a light bulb moment and understood that anytime I'm interacting with the controller in applications, I'm typically interacting with an instance of that class.
00:05:00.000 Now, we have this instance method of dispatch. We have a new instance, and we can save the arguments as instance variables so that we can access them whenever we need to. This will be helpful since the controller's role involves processing requests and responses.
00:05:14.060 Now we need to call our action. Ruby has a couple of ways to call a method. Say we have a class called Greeting and it has an instance method called hello. We can call it in the familiar way using the dot notation like this: greeting.hello, which will return the expected string, 'Hello, how are you today?'
00:05:28.650 Ruby also has another way to send a method, which is by using the send method. If we supply the send method with the name of the method we want to call, either as a string or a symbol, it will call the method, returning the expected string. Now we can use the send method here to call the action.
00:05:46.000 With this collective code, we now have the Base and API controller classes responding to router requests, calling the actions in our controllers, and returning the status headers and body of our response to the router. As we are dispatching requests correctly to our controllers, we still have failures, but they are new failures.
00:06:06.730 Here are two from the API controller—both undefined: one for params and one for render. This is due to the fact that our controllers reference params and render, but our base classes don't have an implementation of either. We'll start by implementing params, which is a nice abstraction that ActionController provides for accessing the parameters of requests.
00:06:30.180 It allows you to grab some directly, require some data in the params, or permit attributes to be passed on to models or classes. The request stores parameter information in a hash, with different access methods.
00:06:41.860 Here are the params for calling the show endpoint for an API. These two uses of params occur throughout our app's controller, so we have two core modeling failures for params. For the first failure, given that the request provides parameters in a hash, we're in luck: simply pulling an ID from that hash suffices.
00:06:57.330 So if we want to get the params for that kind of method, we can simply write a method like this. I've put this method into a Params module, which can be included in our base controller classes to add this functionality.
00:07:12.650 Now, this begs the question: why did I put the code into a module and not directly into our classes, like the Base classes or SuperBase? The main reason is that I used a module to create code boundaries. My API class wants to know what it means to be an API. Does it need to know about params? Sure, but should it be responsible for those details? Probably not.
00:07:30.320 By providing a Params module, I'm stating that these methods and this code provide behavior related to params. This allows me to include it in my API controller class, extending that functionality and focusing solely on creating methods in my base class or SuperBase class that are direct responsibilities of those classes.
00:07:42.460 This is using modules as another way of sharing code. With the API and base classes including this module, its functionality will pass our first params test. However, our use of strong parameters is still failing, and we need a better implementation to make calls to params work.
00:08:02.350 Specifically, we need it to handle parameters that ensure the required attributes exist and filter out unpermitted attributes. Since `require` and `permit` are not methods that exist for hashes, we need to create an abstraction over the request params hash.
00:08:17.120 To produce a working implementation, we could write something like this. What we have here is a new Parameters class initialized by storing the parameters hash as an instance variable, and we write the hash access method to pull from that instance variable.
00:08:32.500 Then our params method simply returns a new Parameters object. This provides us with the functionality we expected. Our next step is to implement `require`. When we call `require`, we're indicating that the specified key must be present in the top level of the params hash.
00:08:49.180 If it is, the method returns its values. If the required item is not present, like if we require `author` for this hash, it should raise an error. This is similar to the `fetch` method for hashes, where if the key is provided, it returns the data, and if not, it raises an error.
00:09:05.520 To provide a basic working implementation, we can implement this method using `fetch`, which may look as follows; however, we need to make sure that its return can also respond to `permit`. This is simply achieved by wrapping the results of `fetch` in an instance of Parameters.
00:09:22.809 This gives us working functionality for `require`. Next, we need to implement `permit`. Similar to `require`, `permit` indicates allowed attributes that can be passed in, filtering out anything else.
00:09:38.490 If we take this hash and say we only allow ‘title’, it should return a hash containing just the title. We're implementing this now since we’re passing in an array of attributes. The method will take an array of attributes, and we can use Ruby's `select` method to filter the params based on those keys.
00:09:50.360 This gives us a passing implementation of `permit`. Our code will look like this, giving us direct access with `require` and `permit` implemented. This leaves us with our render failure for our API specs.
00:10:08.620 To reproduce the render functionality, let's consider what it means in terms of updating our response object and what the response object will return to the router. We know that we want this hash to be the body of the response in a JSON-encoded stream.
00:10:25.700 We also want to provide the status to be the status code of the response. Since this is a JSON response, we want to update the content type to be JSON in the headers.
00:10:37.290 Thus, in our render implementation, we need to provide the response object with these three things: the status, the content type, and the body. This code can become the foundation for our rendering module. We can start implementing the status code.
00:10:55.750 The status is not something we always set when rendering JSON, for instance. When we render JSON for a single post, I don't specify a status code, but rather depend on the default 200 status code for the response. Since the status will always be provided, we can set this as an optional argument.
00:11:09.970 If the argument is provided, we can set the status on the response. It’s as simple as that. We don’t actually have to worry about whether we've set the status as a symbol or the actual status code number; both will work. This is handled by the response object.
00:11:26.250 Setting the content type is as easy as using the content types provided by the response. Lastly, we need to set the body. We must take this hash and turn it into a JSON string.
00:11:43.650 This is easily done by calling `.to_json` on the hash and then assigning it to the response. This means that setting the body should look like this. By working through this, we have a functioning render method in our API.
00:12:00.890 We can incorporate this into the API class, and with our params and rendering modules defined, along with dispatching for our controllers, we achieve a working implementation of AwesomeController API.
00:12:19.460 This means we finally have our API tests passing, and it is processing JSON responses just as before. This leaves us with the web portion of the application to tackle.
00:12:36.060 There is a lot of complexity to rendering web requests that API requests may not typically have. For starters, the web controllers of the app fail because we did not have an implementation of controller callbacks.
00:12:55.240 They also fail because nothing was rendering. Just like in our API, the rendering issue is due to the fact that in our web controllers we’re not calling render explicitly.
00:13:10.380 What we have built for our API will not work in our web requests. This means that I need to implement two new features: controller callbacks and implicit rendering.
00:13:23.000 Callbacks are the easier concept to start with. They allow us to run certain methods before actions, after actions, or around actions. They are analogous to callbacks set for ActiveRecord models. This similarity isn’t coincidental; both types of callbacks leverage a built-in Rails callback functionality provided through ActiveSupport called callbacks.
00:13:40.110 The ActiveSupport callbacks API has three major features: first, it defines a callback set, which allows us to establish a set of callbacks specific to controllers. Once we have a callback set, the next feature permits us to add callbacks to it.
00:13:58.670 Lastly, the third feature involves executing the callbacks. ActionController and ActiveRecord provide abstractions allowing us to only do one of these things in our controllers and models, which is adding a callback to the set. This is what happens when your controller has a before_action.
00:14:16.220 However, the method `before_action` itself is a controller method that serves as an abstraction over that ActiveSupport API. To begin with, we can create our callbacks module and include the ActiveSupport callbacks module in so we can utilize its behavior.
00:14:34.050 Next, we’ll define a set of callbacks for our controllers. Now that we have a set, we want to add our callbacks to it. This will take the form of writing our before, after, or around action methods, using before_action as an example.
00:14:50.690 In order to implement before_action, we need to set the callback using the callback API's set_callback method. Here’s an example of our before_action implementation and what the corresponding set_callback call would look like.
00:15:09.650 In set_callback, we define the action, store the callback type as a before callback, and set the provided method name as a reference from the before_action's statement. The method definition would look something like this.
00:15:23.230 In the end, we would also have around_action and after_action methods, but their definitions only differ in the keywords before, around, and after.
00:15:41.490 Before we finish with callbacks, we actually need to run them. To do that, let's think at a high level about how callbacks are executed. We have our dispatch method, and we know that before actions run prior to the action.
00:15:59.440 So we'll create a temporary method to call our before actions, and around actions will run around the actual action while yielding to it at some point. After actions will run after the action. This is where callbacks are essentially executed.
00:16:11.180 If we wanted to simplify this logic, we could extract the callback handling into one method, creating an all-encompassing callbacks method that calls the before actions, yields for the around actions, and runs the after actions subsequently.
00:16:30.920 We would call that method in our controller instance, similar to what the run_callbacks method does, provided by ActiveSupport callbacks. This runs our callbacks, but we have our callbacks code written into its own module and want to continue doing that.
00:16:47.160 To execute the callbacks within this method without inserting code directly here, we need to have a place to hook our callbacks into this dispatch method. We can provide a hook for action processing by moving the selection into its own method in our base class.
00:17:07.810 Then, calling the method in dispatch, we can define a process method, responsible for making that send action call. In our module, we can define a process method that would run the callbacks and then super to continue with the necessary processes.
00:17:24.920 This works because when process is invoked in our AwesomeControllerBase, it will first go through the process methods defined in the included modules, which will initiate our callback processing before reaching the definition in our base class.
00:17:40.930 In the end, this is what the code for our callbacks module looks like, which we can include in our AwesomeControllerBase class. This gives us a working definition of callbacks.
00:18:01.530 Now, this leaves us with a final issue: the web pages are not rendering anything; they essentially look like empty slides. This is because they are not invoking render. We need to implement the concept of implicit rendering.
00:18:16.440 Implicit rendering means that code can exist in the controller without explicitly calling render, yet the response will still have a rendered body. To implement this, we need to know when calling send action triggers rendering and if it does not, perform a default render.
00:18:35.060 Again, we can leverage the new process method by creating an implicit rendering module that defines the process method. This method lets super or other processes complete their tasks, and when it returns to this method and nothing has been rendered, we handle the default.
00:18:49.830 This can be achieved by tracking rendering with an instance variable, setting it to true at the end of rendering within our implicit rendering module. We can replace the process method with a check that uses this instance variable to see if rendering has occurred.
00:19:11.630 If it hasn't, we can call render. I've put 'render' as a to-do for now because we have not built render for our ActionControllerBase yet. The rendering process is a significant dependency and non-trivial to implement.
00:19:27.370 Rendering HTML differs greatly from rendering JSON. JSON simply involves turning something into a JSON string, but HTML rendering may involve multiple steps to render one page. Implementing this ourselves would be quite a heavy lift.
00:19:46.520 Fortunately, we can leverage another Rails module called ActionView. ActionView manages layouts and templates in Rails. Specifically, we can use a module in ActionView called rendering that provides methods which, given the right arguments, will perform the work required to render HTML.
00:20:07.160 Integrating the controller with ActionView requires several configurations, but I'll highlight some of the key requirements. The first step is providing ActionView with the view paths for our controller. ActionView needs to know where the layouts and templates reside.
00:20:26.630 Since the views are the same, we tell ActionView that they live in the `app/views` directory. Next, ActionView needs to know the variables to assign to the view.
00:20:42.530 I found this particularly interesting. To pass variables from the controller to the view, we can call the instance_variables method from inside the controller. We can build a hash of them, using the variable names as keys and their values accordingly.
00:20:59.380 When ActionView receives this hash, it effectively reverses the process: it creates an instance variable with the key name and provided value. Thus, the controller has successfully shared its instance variables with the view.
00:21:12.160 Several methods needed to be defined to work with the ActionView rendering module, which overall results in the rendering code being distributed across two modules.
00:21:29.950 After putting together all these details and method definitions, I managed to call `render`, which is provided by the ActionView rendering module, which returned the rendered HTML.
00:21:50.000 This HTML output looks something like this. We can glimpse how ActionView processes the renderings. ActionView takes a template and converts it into a unique method.
00:22:09.500 Notice how the method contains the view names. If we zoom in on a specific part, such as rendering the title for a post, it separates the static strings from the dynamic variable or method calls.
00:22:17.890 It compiles each line and appends it to a buffer. At the end, it converts that buffer into a string, which we have set as the response body. Rendering CSS makes it look like this.
00:22:39.350 With all of this completed, we now have a fully functioning web controller, and all our tests are passing. It may have been ambitious to name it AwesomeController; a more fitting name might be MinimumController or CoolIdeaController.
00:23:01.420 However, don’t actually use this in production. The code written here addresses the major concepts of a controller. At the heart of the controller are requests and responses, and everything the controller does relates to those.
00:23:17.300 While this code does not suffice for running in production, I hope that the concepts explored here provide new insights into controllers, their modules, behaviors, and their role in the Rails request lifecycle.
00:23:37.350 Thank you for watching my talk! The code lives on GitHub, so please take a look at it and play around with it—that's what I made it for. The code really helped me navigate through this talk.
00:23:54.370 There are several other modules included that I was unable to cover, and I hope you take a look at those as well. The repo also includes Puma, Rails, and Rack in the dependencies folder, so you can dive into those.
00:24:12.530 The repo also has the Pry By Bug gem. I found it useful since it allows me to step into the code for insightful debugging. Finally, I encourage you not to stop here.
00:24:31.500 Dig through the controller code in Rails, which resides in the action-pack module, and I guarantee you'll walk away with new learnings. If you found this talk helpful, please reach out and let me know! I'm on Twitter, and any feedback is appreciated.
00:24:50.000 This has been a topic I've explored for a long time, and it would bring me joy to know that this talk was helpful to you. Thank you again!