Building an MVC Framework — Part 1
Around a year ago, I started teaching ruby and rails at Andela. One of the biggest challenge in teaching rails is explaining all of the magic that Rails uses to do its job . The most effective way to really understand how things work in rails is to rebuild it from scratch.
This is my attempt at building an MVC framework similar to rails to teach my fellows how rails work?
This series is broken down into multiple posts which I will be posting gradually. The posts are:
- Part 1 — Rack Deep Dive
- Part 2 — Set up a Basic Framework
- Part 3 — Autoloading and Utility Methods
- Part 4 — Better Routing
- Part 5 — Render, Redirect & Before_Action Methods in Controllers
- Part 6 — Extract Complexities out of View with Helpers
- Part 7 — Reduce Rework with Generators
- Part 8 — Set up ORM for Read/Write to Database
- Part 9 — Generate Database from Schema
- Part 10 — Set up Migrations
For this series of posts, I’m going to assume that you have a general understanding of HTTP (for example: 2xx status code means success) and Ruby, though you may never have built a web framework before.
To understand Rails and even the more basic Sinatra, I think we need to go deeper and start with Rack.
What is Rack
Did you know that rails and sinatra are rack apps, so also is our future MVC framework. Then, what exactly is rack and how does it work.
In a sentence, Rack provides a minimal interface between webservers that support Ruby and Ruby frameworks. It is a Ruby package that provides an easy-to-use interface to the Ruby Net::HTTP library.
All major Ruby web servers (Puma, WEBrick, Unicorn, etc) understand the Rack protocol, so if our app conforms to the Rack application specification, we can use those servers with it for free.
Since our MVC framework will be a rack app, understanding rack and it’s specification is necessary before we can proceed towards building our framework.
From rack website(http://rack.github.io)
To use Rack, provide an “app”: an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements:
- The HTTP response code
- A Hash of headers
- The response body, which must respond to each
You basically provide an object(not a class) that responds to call, passing in another object when the call method is called which will return another object. It is all ruby.
To fully understand rack and it’s specification, let’s build a rack app.
A Tiny Rack App
We are going to build a rack app with 3 lines of code. Create a file called tiny_rack_app.rb and add the following content to it.
In the first line, we required rack. In the second line, we created an object that responds to call. Remember that a proc has a call method and it executes the block passed to it during initialization. Notice also that the block passed to the proc accepts `env` as an argument and returns a rack compatible response(from specification).
The third line is used to boot our ruby server. You will notice this line:
Rack uses handlers to run Rack applications. Each Ruby webserver has its own handler, but I chose the WEBrick handler for this example, because WEBrick is installed by default with Ruby. You can use other handlers like Thin which is installed by default or you can use web servers like `Puma` and `Unicorn` by installing and requiring the necessary gems. Run it using:
$ ruby tiny_rack_app.rb
and navigate to localhost:9292. You will see `I respond to all request` in your browser.
We can also use the rackup command line tool(with config.ru file) and avoid specifying details like port and server until runtime.
In this case, I am using a lambda instead of a Proc. Run it using
$ rackup --port 9292
and navigate to localhost:9292. You will see `I respond to all GET request` in your browser.
Another Rack App
Here is a rack app that is created from a custom class that responds to call method and that uses puma handler. Remember to install puma gem using gem install puma before using this handler.
Paste the above code in a file called `another_rack_app.rb` and run it using
$ ruby another_rack_app.rb
Some interesting things are happening in the call method. It accept’s an environment hash as an argument(like any other rack app), then gets the verb and path from the env hash. In `line 8`, we set the response `content-type` header to be `text/html` and constructed a html response body in `line 9`. The final content returned in `line 10` is a rack compatible response.
The final content returned in `line 10` is a rack compatible response.
Our Rack server object takes in an environment hash. What’s contained in that hash? Here are a few of the more interesting parts:
- REQUEST_METHOD: The HTTP verb of the request. This is required.
- PATH_INFO: The request URL path, relative to the root of the application.
- QUERY_STRING: Anything that followed ? in the request URL string.
- SERVER_NAME and SERVER_PORT: The server’s address and port.
- rack.version: The rack version in use.
- rack.url_scheme: is it http or https?
- rack.input: an IO-like object that contains the raw HTTP POST data.
- rack.errors: an object that response to puts, write, and flush.
- rack.session: A key value store for storing request session data.
- rack.logger: An object that can log interfaces. It should implement info, debug, warn,error, and fatal methods.
You can wrap the environment hash in a Rack::Request object to work with the environment hash in a more object oriented way. eg.
Accessing env directly quickly becomes tedious in any Rack project, more complex than AnotherRackApp. You should use Rack::Request
Middleware gives you a way to compose Rack applications together.
In the real world, your rack app won’t work in isolation. More often you want to process the request or response before it hits your final rack app. Middleware
Middlewares are other Rack applications that comes between our final app and the HTTP request. Middleware is Rack’s true strength.
For example, if you have a Rails app lying around (chances are, if you’re a Ruby developer, that you do), you can cd into the app and run the command rake middleware to see what middleware Rails is using:
$ cd my-rails-app
$ rake middleware
Below is an example middleware.
The difference between a middleware and your final rack app is that it’s initialized with an app, which is the “final” Rack app that it can pass the request on to.
The middleware above updates our request method(verb) with the value of `method` query param if it is set. So we can we can make a get request and change it to a post request like this: `localhost:9292?method=post`.
You can combine your middleware with your rack app using `Rack::Builder`.
For example, you can combine the MethodOverrideMiddleware middleware with the AnotherRackApp rack app above using
Try putting the middleware code and the builder code in another_rack_app.rb file and remove the following lines.
Run it with
$ ruby another_rack_app.rb
You are not limited to one middleware. You can stack as many middleware as possible as seen in rails.
With this, we are done with the discussion on rack.
Keep calm. In the next post, we will start building out the MVC Framework.
If you liked this, click the💚 below so other people will see this here on Medium. Also, if you have any question or observation, use the comment section to share your thoughts/questions.