03 Rest - kjellhex/diode GitHub Wiki

Code

Let's build a simple REST server that supports GET enquiries and POST updates, and also see how easily request parameters can be accessed.

Server

We can start with a minimal server:

# status-server.rb
require 'diode/server'
routing = [
  [%r{^/status$}, "Status"]      # GET and POST
]
settings = {
  "category": "development"
}
load 'status.rb'
diode = Diode::Server.new(3999, routing, settings).start()

This will respond to a GET or POST request with a path matching exactly /status and that is all. Anything else will return a standard HTML 404 response.

Service

The Status service maintains the value of the class variable @@operationalStatus and either returns the current value in response to a GET request, or updates the status in response to a valid POST request:

class Status
  @@operationalStatus = :online
  @@validStates = [:online, :quiet, :offline]

  def serve(request)
    request.method == "GET" ? get(request) : post(request)
  end

  def get(request)
    h = { "operationalStatus": @@operationalStatus.to_s() }
    h["category"] = request[:category] if request.params["extra"].to_s == "true"
    Diode::Response.new(200, JSON.dump(h), {"Cache-Control" => "no-cache"})
  end

  def post(request)
    return badRequest() unless request.headers["Content-Type"].downcase() == "application/json"
    payload = JSON.parse(request.body)
    newState = payload["state"].to_sym
    unless @@validStates.include?(newState)
      p :badstate, payload, request # so we can see what the payload contains
      return badRequest()
    end
    @@operationalStatus = newState
    Diode::Response.new(200, JSON.dump({"status": "updated"}))
  end

  def badRequest()
    Diode::Response.new(400, JSON.dump({"status": "bad request"}))
  end

end

Running

Start the server:

$ ruby status-server.rb

and visit http://127.0.0.1:3999/status to see the initial status:

{ 
  "operationalStatus": "online" 
}

and to see more details, add a parameter: http://127.0.0.1:3999/status?extra=true

{ 
  "operationalStatus": "online" ,
  "category": "development"
}

To update the status, POST a request to http://127.0.0.1:3999/status with a body:

{ "state": "offline" }

How does it work?

Application state

Common options for maintaining some state in an application include using global variables (which can be accessed in any service), class variables (available only in instances of that service), or an external location such as database or file system. In this case, we store the status in a class variable for demonstration purposes:

class Status
  @@operationalStatus = :online
  @@validStates = [:online, :quiet, :offline]

HTTP Methods

Every service must implement the serve method. Since we want to support both GET and POST, let's handle them in separate methods:

  def serve(request)
    request.method == "GET" ? get(request) : post(request)
  end

Why does Diode only support GET and POST? Why not PATCH and HEAD and PUT? For two reasons:

  1. Most web servers do not allow methods like PATCH and PUT, and Diode is designed to be placed behind a web server like nginx. Although nginx can be configured to allow more methods, it often isn't and you may not be allowed to change the nginx configuration for organisational, legal, or security reasons. On the other hand, GET and POST are (almost) always allowed.
  2. The security philosophy of Diode is to keep it secure by keeping it small and simple. Diode has not yet encountered any decent argument why anything more than GET and POST is needed. GET makes sense for an enquiry that does not change the underlying data. POST makes sense to any request that does change the underlying data, whether that change is creating, updating, or deleting a record, or updating just one field in a record. If however you really need those HTTP methods (for example, you want to build a mock server to test an existing REST client that uses those methods) then feel free to fork Diode and change the Diode::Request to allow more methods.

Request parameters

The get method handles GET requests and responds with a JSON payload containing the current operational status. If the request contains a parameter with the name extra having a value of "true", then another field is added to the response payload. The category field is added to the JSON with the value of an application setting. Pay attention to the difference between the "strings" and :symbols. Recall that the application settings used the format which makes the key a symbol :category so it must be accessed as request[:category] not request["category"]. JSON is more lenient however so you can add a field using either a string or symbol as the field name.

def get(request)
  h = { "operationalStatus": @@operationalStatus.to_s() }
  h[:category] = request[:category] if request.params["extra"].to_s == "true"
  Diode::Response.new(200, JSON.dump(h), {"Cache-Control" => "no-cache"})
end

The response also includes a third parameter to override the value of the Cache-Control header.

Bad Request

The post method validates the request and if the request fails that validation then a 400 Bad Request response must be sent. Since that happens a couple of times, its worth extracting it into its own method:

def badRequest()
  Diode::Response.new(400, JSON.dump({"status": "bad request"}))
end

JSON payload

The post method first checks that the posted body is JSON, and then tries to parse it.

return badRequest() unless request.headers["Content-Type"].downcase() == "application/json"
payload = JSON.parse(request.body)

Next it obtains the new state from the payload and checks if the value is valid. The string value is converted into a symbol. If the value is not one of the valid states, then the payload and the request is inspected so the developer can see all the values that were received, and the Bad Request response is returned:

newState = payload["state"].to_sym
unless @@validStates.include?(newState)
  p :badstate, payload, request # so we can see what the payload contains
  return badRequest()
end

If validation was successful, then the state is updated, and a response generated:

@@operationalStatus = newState
Diode::Response.new(200, JSON.dump({"status": "updated"}))

Congratulations! You can build REST APIs.

Now its time to make your services database-driven with the 04 Database tutorial.