This article is a short study in web application design. We will be comparing Rails to Django, using a simple web form as an example.

Rails

Let’s begin with some typical Rails code.

# A model, with a custom validation class Post < ApplicationRecord validate :body_includes_title def body_includes_title unless body . include? ( title ) errors . add ( :base , 'Body must contain title' ) end end end # A controller for rendering a form, and handling its submission class PostsController < ApplicationController before_action :set_post def edit end def update if @post . update ( post_params ) redirect_to @post else render :edit end end private def set_post @post = Post . find ( params [ :id ]) end def post_params require ( :post ). permit ( :title , :body ) end end

<%# A view that renders the form, displaying errors per-field %> <%= form_for ( @post ) do | f | %> <%= @post . errors [ :base ]. full_messages %> <%= f . text_field :title %> <%= @post . errors [ :title ]. full_messages %> <%= f . text_area :body %> <%= @post . errors [ :body ]. full_messages %> <% end %>

Django

Here is the same functionality, translated into Django.

Disclaimer: I am not a Django developer, so take this implementation with a grain of salt.

from django.db import models from django import forms from django.shortcuts import render , redirect # The model class Post ( models . Model ): title = models . CharField () body = models . CharField () # The form, with a custom validation class PostForm ( forms . ModelForm ): class Meta : model = Post fields = [ 'title' , 'body' ] def clean ( self ): cleaned_data = super () . clean () body = cleaned_data . get ( "body" ) title = cleaned_data . get ( "tags" ) if not title in body : self . add_error ( None , "Body must contain title" ) # The request handler (equivalent of a controller) def update_post ( request , post_id ): post = get_object_or_404 ( Post , pk = post_id ) if request . method == 'GET' form = PostForm ( instance = post ) else form = PostForm ( request . POST , instance = post ) if form . is_valid (): post = form . save () return redirect ( post ) return render ( request , 'update_post.html' , { 'form' : form })

<!-- The view that renders the form --> <form action="/posts/ {{ form . id }} " action="POST"> {{ form }} </form>

<!-- A different way to write the view, with more control over rendered HTML --> <form action="/posts/ {{ form . id }} " action="POST"> {{ form . non_field_errors }} <input name="title" value=" {{ form . title }} " /> {{ form . title . errors }} <textarea name="body"> {{ form . body }} </textarea> {{ form . body . errors }} </form>

Rubified Django

Before we dive into the comparison, let’s translate the Django code into what it might look like if it were implemented in Ruby.

# The model class Post < Django :: Models :: Model attr :title , Django :: Model :: StringField . new attr :body , Django :: Model :: StringField . new end # The form class PostForm < Django :: Forms :: ModelForm model Post fields [ :title , :body ] def clean attrs = super unless attrs [ :body ]. include? ( attrs [ :title ]) add_error ( nil , "Body must contain title" ) end end end # The controller module PostsController extend Django :: Shortcuts # provides `render` and `redirect_to` methods def self . update ( request , post_id ) post = Post . find ( post_id ) if request . method . get? form = PostForm . new ( instance: post ) else form = PostForm . new ( request . params , instance: post ) if form . valid? post = form . save return redirect_to ( post ) end end return render ( request , 'posts/edit.html' , form: form ) end end

<%# The view %> <%= form_for ( @form ) do %> <%= @form %> <% end %>

<%# A different way to write the view, with more control over rendered HTML %> <%= form_for ( @form ) do %> <%= @form . non_field_errors %> <input name= "title" value= " <%= @form . title %> " /> <%= @form . title . errors %> <textarea name= "body" > <%= @form . body %> </textarea> <%= @form . body . errors %> <% end %>

Observations

Python tends to use namespaced constants

Ruby doesn’t really have a module system. The require method is basically just eval , running the contents of a file within the global namespace. All Ruby files have access to all other Ruby files that have ever been require d previously. This means we don’t write a lot of require s at the top of each file, but all code can affect, and be affected by, all other code.

Python has a more sophisticated module system. Each file is a module, and must explicitly declare which other modules it wants access to. This means that each file has a bunch of import s at the top, but can’t accidentally interact with all other code in the application.

Django models contain attribute definitions

Rails model classes load their attributes from the database at runtime. The benefit of this approach is that the model attributes always reflect the columns of their database table. The downside is that we can not programmatically access the attributes of a model without a database connection.

Django has built-in form objects

Rails makes no distinction between models and forms, at least in the controller. Model objects are used to render forms, and controllers pass user input directly into model objects for validation.

Many Rails projects make use of form objects, but Rails does not provide them out of the box. ActiveModel::Model can be used to make form objects fairly easily, but still, forms are not a first-class citizen.

Django comes with many different kinds of form objects. There a generic Form class, which can coerce and validate HTTP params without being tied to any model. Then there are various different ModelForm classes, which act upon model objects.

Django forms can take fields from models

A common complaint about Rails form objects is that they duplicate validation logic that already exists in models.

Django forms take their fields and validations from models objects, limiting duplication. They are not limited to the fields in the model, though. They can take a subset of the model fields, add extra fields of their own, and replace model fields with different ones.

Django controllers are functions

In Rails, request handlers are implemented as methods (actions) on controller objects. The HTTP request and response are part of the mutable internal state of each controller object. This design means that action methods take no parameters, and have no meaningful return value.

In Django, request handlers are simple functions. The HTTP request is passed in as an argument, and the return value is the HTTP response. There is no need for the handler to be a method on a class, or to inherit from anything. This is a functional design, much like Rack.

Django does not route based on HTTP method

GET and POST requests to the same URL are usually routed to separate controller actions in Rails. For forms, both actions usually need to load the model with something like @my_model = MyModel.find(params[:id]) . This duplication is often removed with a before_action .

Django sends GET and POST requests to a single request handling function. The function then looks at the HTTP method on the request object, and acts appropriately. This removes the need for something like before_action , which Django’s design would not be able to accommodate easily, anyway.

Roda removes this duplication in a similar way, with its nested routing:

r . on "blog_post" , Integer do | id | blog_post = BlogPost . find ( id ) r . get do # render the form end r . post do # handle the submitted form end end

Django forms render themselves

Django form objects can render themselves into HTML. The form’s field objects have various different options which control how they are rendered.

Rails takes a more explicit approach, using FormBuilder s to generate HTML based on model objects. Automatic form rendering wouldn’t be feasible without control over which fields to render, at which point you basically have form objects.

Django forms contain business logic

Django ModelForm objects perform their task by calling the save method, which returns the updated model object.

With the ability to render themselves, this makes Django forms a strange blend of model, view and business logic responsibilities. Put another way: they are complicated.

Form objects are typically implemented this way in Rails, too, but they usually don’t render themselves.

Django form errors are on the fields

Django renders values and errors using field objects, something like this:

{{ post.title }} {{ post.title.errors }}

In contrast, each Rails model has a ActiveModel::Errors object, used something like this:

<%= @post . title %> <%= @post . errors [ :title ] %>

For errors that don’t belong to a single field, Django uses a method on the form:

{{ form.non_field_errors }}

Whereas Rails uses the :base error key as a sentinel value:

<%= @post . errors [ :base ] %>

Strong params are not necessary

Passing unfiltered params into model objects results in mass assignment vulnerabilities. Rails was embarrassingly burnt by these vulnerabilities in the past, resulting in the strong parameters feature.

Django form objects explicitly declare all the fields that they accept. Any extra fields present in the request params are ignored, preventing mass assignment.

Django view arguments are explicit

Rails passes view arguments by magically copying instance variables from the controller. It is possible to pass view arguments explicitly in Rails with render locals: {...} , but controller instance variables are the preferred choice. This leads to a difference between normal views using instance variables, and partials which require locals.

Django passes view arguments explicitly. There is no distinction between normal views and partials.

Django also has mutable errors

Both Rails and Django have designed their validation with a mutable set of errors. Models and forms start with an empty error set, then errors may be added to the set during validation, and afterwards the object is valid if the error set is still empty.