Are 'New' and 'Edit' RESTful? (Rails)

This isn’t meant to be a long post, but I do want to take the time to explore the ‘new’ and ‘edit’ default Rails controller actions and :resources routing defaults as it relates to “RESTful-ness” and/or resourceful routing. Let’s start with what the Rails guide notes:

The foundation of RESTful routing is generally considered to be Roy Fielding’s doctoral thesis, Architectural Styles and the Design of Network-based Software Architectures. Fortunately, you need not read this entire document to understand how REST works in Rails. REST, an acronym for Representational State Transfer, boils down to two main principles for our purposes:

  • Using resource identifiers (which, for the purposes of discussion, you can think of as URLs) to represent resources
  • Transferring representations of the state of that resource between system components.

For example, to a Rails application a request such as this:

DELETE /photos/17

would be understood to refer to a photo resource with the ID of 17, and to indicate a desired action – deleting that resource. REST is a natural style for the architecture of web applications, and Rails makes it even more natural by using conventions to shield you from some of the RESTful complexities.

It goes on to note how the :resources command within the routes file generates these routes (following RESTful design):

chart

Two routes stand out to me in this image. These two:

chart h

Lets walk through them so I can explain why.

  • GET to /photos. Canonically this would be the spoken equivalent to “get all of the photos”. Makes sense to me!
  • POST to /photos. This one’s spoken equivalent would be more like, “here’s a new one, add it to the group”. I can get on board with that.
  • GET to /photos/1. Continuing the spoken-trend, this would be like “give me the details for this singular unit of the group (photo)“.
  • PUT to /photos/1. Speaking this one out would probably be, “on this already-existing unit (photo), update the information on it”.
  • DELETE to /photos/1. Maybe the simplest of all of them, “delete this already-existing unit (photo)“.

The important part about all of these actions and routes is that they are a pure abstraction of action and path without care toward visual interface (UI). Any system should be able to leverage these routes to understand and interact with the full data model for your application. They’re simple — “give me (a) thing(s)” or “do something to a thing” and don’t impose or presume any knowledge of how the stuff looks, only how the under-the-hood request(s) need to (a) use the right HTTP verb, and (b) use the correct URL path.

That’s where edit and new differ. Let’s look at those ones. First, a GET to /photos/new. What is that actually doing from a RESTful standpoint? What ‘state’ or information are we transferring to the client by answering the request? Actually none. It’s an additional (not-RESTful) route designed to provide an interface for a subsequent RESTful action for systems/users that need guidance in how to interact with your application. Same idea with a GET to /photos/1/edit — an additional path to provide an interface for a subsequent PUT to /photos/1 without actually transferring any information (yet).

If you take a look around outside the Rails ecosystem and read some of the generic “what’s a RESTful API anyway?” type stuff, you’ll see no mention of new or edit there, but perhaps the best proof I can provide is a fresh Rails app itself. Spin up a fresh Rails 6.1 app and scaffold a model:

rails g scaffold House address rooms:integer

Then pop open houses_controller.rb and you’ll find these actions:

  # GET /houses or /houses.json
  def index
    @houses = House.all
  end

  # GET /houses/1 or /houses/1.json
  def show
  end

  # GET /houses/new
  def new
    @house = House.new
  end

  # GET /houses/1/edit
  def edit
  end

  # POST /houses or /houses.json
  def create
    @house = House.new(house_params)

    respond_to do |format|
      if @house.save
        format.html { redirect_to @house, notice: "House was successfully created." }
        format.json { render :show, status: :created, location: @house }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @house.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /houses/1 or /houses/1.json
  def update
    respond_to do |format|
      if @house.update(house_params)
        format.html { redirect_to @house, notice: "House was successfully updated." }
        format.json { render :show, status: :ok, location: @house }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @house.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /houses/1 or /houses/1.json
  def destroy
    @house.destroy
    respond_to do |format|
      format.html { redirect_to houses_url, notice: "House was successfully destroyed." }
      format.json { head :no_content }
    end
  end

Interesting. Notice how the comments above index, show, create, update, and destroy all call out a .json view format? Indeed index.json.jbuilder, show.json.jbuilder, and _house.json.jbuilder were all created for us. But the comments above new and edit don’t note any .json format and the only views generated for new and edit were new.html.erb and edit.html.erb. Why?

Because users operating on an HTML interface typically don’t have mechanisms for executing RESTful actions directly. So Rails (being an excellent framework at making us productive) gives us two routes beyond standard REST to give users without RESTful interfaces a means for executing RESTful actions. Typically this is in HTML, but it doesn’t actually need to be. new should give the user (on whatever platform they’re on) the necessary means or instructions for how to successfully create a new house by POSTing to the path /houses. edit should give the user (on whatever platform they’re on) the necessary means or instructions for how to successfully edit the current values on a given house (id = :id) by PATCHing to the path /houses/:id.

There’s a subtle phrasing there that I want to elaborate on. The goal of /new and /edit is to provide the user with the instructions for how to do a subsequent RESTful action. Sure, that’s typically a Rails form that’s marked up and formatted in such a way that the user can enter data into HTML input fields and the browser will natively follow the Submit action system to send form-encoded data with the correct nesting / format, but it doesn’t have to be. Technically, the HTML page rendered at /new could just be a paragraph explaining how to use cURL to POST to /houses. That would be a valid /new view, from the Rails ethos perspective. Your users certainly wouldn’t appreciate it… and half of them probably wouldn’t understand it, but the point is not that /new and /edit are made to expose forms to execute subsequent requests — they’re made to give the user instructions for how to make a subsequent request. The form is just simplest mechanism of doing that. (And that’s why most forms have syntax like “fill out the fields below then hit submit” just to make it extra clear)

This is all contrasted with typical JSON REST API consumers that do have mechanisms built out for POSTing to /houses or PATCHing to /houses/:id. JSON REST API consumers (one app acting as a ‘consumer’ talking to another app) don’t need a new form to guide them in the correct way to format a subsequent POST. The developers of the consumer instead encoded the correct format into the consumer system’s code so that the consumer system always POSTs correctly every time. Have you ever integrated an app with Stripe? Then you’ve done this. Instead of providing a /new or /edit form every time your application wanted to create a new Payment, Stripe told you (the developer) what format / shape to sent data to the JSON API endpoint in and you encoded that shape into your app directly. Now your app can send the correct shape every time and doesn’t need guidance on how to do so — it was hard-coded by you. This was indeed one of the goals of the REST convention — provide a predictable and reliable formatting for interacting with resources in other systems.

It applies beyond other consumer systems too. Sometimes even us developers like to interact with various services via command-line requests. Understanding RESTful endpoint norms makes that much easier! Netlify, for example, maintains a JSON API that contains essentially all of the functionality of their standard HTML/browser-based management interface. Because I know RESTful norms, I know that I can GET to /sites and expect to get back a list of all of my sites hosted on Netlify. Similarly, I can POST to /sites and expect to make a new one (provided I send the correct information with the request), or PUT to /sites/jonsully and expect to edit/update this site’s attributes. Just by following the resourceful conventions, I can traverse Netlify’s JSON API with ease via command line requests.

Okay Jon… so new and edit aren’t exactly part of the REST API spec… why does any of this matter?

Well, learning of course! Both in the sense of learning about how REST works, how Rails implements REST (and more), and how Rails communicates over various formats is good to know! It can be the difference between implementing a lot of code or just a little code! It’s all part of learning to understand how a single controller can correctly serve a resource with different views, variants, and parameters following the resourceful style. I’ll write more on that in the future.

The answer also comes in my next post/series which gives an option for multi-page forms (“wizards”) in Rails. The method I introduce there overrides the new method on a controller which got me thinking about whether or not that’s “okay” from a Railsy-ness standpoint. I realized that my override is doing the same thing (providing a user with an interface to interact in RESTful ways) just a little differently!

EDIT: The series has been written! Rails Wizards Pt. 1

Header Photo / Banner by: @the_roaming_platypus

Join the Conversation

Latest Blog Posts