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:
- 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.
- 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.