one person's perspective on a pragmatic rest interface
TRANSCRIPT
One Person's Perspective on a Pragmatic REST Interface
perl -d:hdb to your browser
REST
Representational State Transfer
Representational State Transfer
I'm not an expert
I'm not selling a book or consulting service
There's 10 zillion other places to hear from them
Representational State Transfer
I'm not an expert
I'm not selling a book or consulting service
There's 10 zillion other places to hear from them
I'm just a guy who's completed a few projects
I just want to get some work done
Representational State Transfer
What does it mean?
Representational State Transfer
Representational State Transfer
Representational State Transfer
HTTP?
Representational State Transfer
HTTP? No
XML?
Representational State Transfer
HTTP? No
XML? No
JSON?
Representational State Transfer
HTTP? No
XML? No
JSON? No
Web browser?
Representational State Transfer
HTTP? No
XML? No
JSON? No
Web browser? No
Web server?
Representational State Transfer
HTTP? No
XML? No
JSON? No
Web browser? No
Web server? No
Representational State Transfer
Client/Server
Stateless (Server)
Cachable
REST Interface for Widgets
GET - No side effects
GET /widgets/GET /widgets/?price=100GET /widgets/123
REST Interface for Widgets
GET - No side effects
PUT - Idempotent
PUT /widgets/PUT /widgets/123
REST Interface for Widgets
GET - No side effects
PUT - Idempotent
DELETE - Idempotent
DELETE /widgets/DELETE /widgets/123
REST Interface for Widgets
GET - No side effects
PUT - Idempotent
DELETE - Idempotent
POST - The Wild West
POST /widgets/POST /widgets/123?name=Acme
REST Interface for Widgets
GET - No side effects
PUT - Idempotent
DELETE - Idempotent
POST - The Wild West
PATCH - Probably Idempotent
PATCH /widgets/123?name=Acme
REST Interface for Widgets
GET - No side effects
PUT - Idempotent
DELETE - Idempotent
POST - The Wild West
PATCH - Probably Idempotent
HEAD - No side effects
HEAD /widgets/HEAD /widgets/123
REST Interface for Widgets
1XX - Informational
100 Continue
REST Interface for Widgets
1XX - Informational
2XX - Success
200 OK201 Created202 Accepted204 No Content
REST Interface for Widgets
1XX - Informational
2XX - Success
3XX - Redirect
303 See Other
REST Interface for Widgets
1XX - Informational
2XX - Success
3XX - Redirect
4XX - Client Error
400 Bad Request423 Locked401 Unauthorized428 Too Many Requests402 Payment Required403 Forbidden404 Not Found409 Conflict
REST Interface for Widgets
1XX - Informational
2XX - Success
3XX - Redirect
4XX - Client Error
5XX - Server Error
500 Internal Server Error507 Insufficient Storage
What Makes a Good REST Interface
Documentation
What Makes a Good REST Interface
Documentation
Unsurprising
GET/PUT/DELETE should be idempotent/side-effect free
/widgets/ should manipulate Widget with that ID
Use standard error codes where applicable, make up new ones if necessary
What Makes a Good REST Interface
Documentation
Unsurprising
URLs are nouns (/widgets/ID) Request types are verbs (GET/PUT/DELETE)
GET /delete/widget/123
What Makes a Good REST Interface
Documentation
Unsurprising
URLs are nouns (/widgets/ID) Request types are verbs (GET/PUT/DELETE)
Discoverable - Responses include other URLs
Lists should have links to each instanceInstance info could have links to edit/deleteRoot should include URLs to the top-level entities
What I Tried First
Documentation
Unsurprising
URLs are nouns (/widgets/ID) Request types are verbs (GET/PUT/DELETE)
Discoverable
Devel::hdb Built on PSGI/Plack
Perl web Server Gateway Interface
Standard for web servers to run a Perl application
Applications are just a function with one hashref parameter (environment)
Returns a 3-element listref
sub app { my $env = shift; return [ 200, ['Content-Type' => 'text/plain'], ['Hello there ', $env{REMOTE_ADDR} ] ];}
PSGI Environment
REQUEST_METHOD - 'GET'
PATH_INFO - '/widgets/'
QUERY_STRING - 'price=100'
CONTENT_TYPE 'multipart/form-data'
CONTENT_LENGTH 432
HTTP_ACCEPT 'application/json'
HTTP_* - Other standard HTML headers
psgi.input IO::Handle for the request body
psgi.errors IO::Handle for printing errors
PSGI Middleware
One application subref can wrap another
Edit the environment before calling original
Edit headers/body before returning
PSGI Middleware
One application subref can wrap another
Edit the environment before calling original
Edit headers/body before returning
sub add_header { my $original = shift return sub { my $env = shift; my $result = $original->($env); push @{$result->[1]}, 'X-Handled-By' => $ENV{HOST}; return $result; }}
PSGI Middleware
Plack::Middleware Base class for Middleware
P::M::Negotiate Call different apps on Accepts
P::M::OAuth OAuth authentication
P::M::LogErrors Plug Log::Dispatch into psgi.errors
P::M::REPL Start a REPL on console after die
P::M::Cache Memoize for URLs
P::M::Antibot Various mechanisms for preventing bots from submitting forms
PSGI Middleware
PSGI Servers
HTTP::Server::PSGI Plack reference implementation
Starman Popular
Twiggy AnyEvent-based
mod_psgi Apache2 adapter
Others...
PSGI Frameworks
Catalyst Very configurable
Dancer Like Ruby's Sinatra
Mason Old Standby
Mojolicious Self-contained and shiny
Others...
Devel::hdb as a PSGI App
Devel::hdb::Router::route()
sub route { my $env = shift; foreach my $route ( @{$routes->{$env->{REQUEST_METHOD}} ) { my($path, $cb) = @$route; my $fire; If (ref $path eq 'Regexp') { $fire = $env->{PATH_INFO} =~ $path; } elseif ($env->{PATH_INFO} eq $path) { $fire = 1; } return $cb->($env) if $fire; }
Devel::hdb as a PSGI App
Devel::hdb::Router::route()
sub route { my $env = shift; foreach my $route ( @{$routes->{$env->{REQUEST_METHOD}} ) { ... } return [ 404, [ 'Content-Type' => 'text/html' ], [ 'Not Found' ] ];}
Devel::hdb as a PSGI App
Devel::hdb::App::ProgramName
__PACKAGE__->add_route('get', '/program_name', \&program_name);
my $program_name = $0;sub program_name { my $env = shift;
return [ 200, [ 'Content-Type' => 'application/json' ], [ encode_json({ program_name => $program_name }) ] ];}
Anatomy of a Request
Anatomy of a request
mouseenter event triggered
debugger.js: 699
$elt.on('mouseenter', '.popup-perl-var', popoverPerlVar.bind(this))
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
debugger.js: 387
restInterface .getVarAtLevel(varname, stack_level) .done(function(data) { drawPerlPopover(data, $elt));
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
restinterface.js: 52
this.getVarAtLevel = function(varname, level) { var url = '/getvar/' + level + '/' + encodeURIComponent(varname); return this._GET(url, undefined);}
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
Router.pm: 51
foreach my $route ( @$gets ) { my($path, $cb) = @$route; if (@matches = $url =~ $path) { $cb->($env, @matches); }}
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
hdb's do_getvar()
App/Eval.pm: 55
__PACKAGE__->add_route( 'get', qr{/getvar/(\d+)/([^/]+)}, \&do_getvar);
sub do_getvar { my($class,$app,$env,$level, $var)=@_;
my $value = eval { $app->get_var_at_level($var, $level); }; }
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
hdb's do_getvar()
Devel::Chitin and PadWalker
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
hdb's do_getvar()
Devel::Chitin and PadWalker
Encode result and return
App/Eval.pm: 55
sub do_getvar { my $value = eval { $app->get_var_at_level($var, $level); }; my $encoded = Data::Transform::ExplicitMetadata::encode($value) return [ 200, [ 'Content-Type' => 'application/json' ], [ encode_json($encoded) ] ];}
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
hdb's do_getvar()
Devel::Chitin and PadWalker
Encode result and return
Plack generates response
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
hdb's do_getvar()
Devel::Chitin and PadWalker
Encode result and return
Plack generates response
Promise is kept
debugger.js: 387
restInterface .getVarAtLevel(varname, stack_level) .done(function(data) { drawPerlPopover(data, $elt));
Anatomy of a request
mouseenter event triggered
Call getVarAtLevel()
Send GET request
Server receives GET
Plack encodes the request
Router middleware
hdb's do_getvar()
Devel::Chitin and PadWalker
Encode result and return
Plack generates response
Promise is kept
Draw the popover
debugger.js: 330
function drawPerlPopover(data) { var perl_val = PerlValue.parseFromEval(data); var popover_args = { Trigger: 'manual', Placement: 'bottom', Container: $elt, Content: perl_val.render_value() }; $perlPopOver.popover(popover_args) .popover('show');}
Another request
Another request
Click event
debugger.js: 698
$elt.on( 'click', '.control-button[disabled!="disabled"]', this.controlButtonClicked.bind(this))
Another request
Click event
Call stepin
debugger.js: 300
function controlButtonClicked() { ... restinterface .stepin() .done(handleControlButtonResponse);}
Another request
Click event
Call stepin
Send POST request
restinterface.js: 104
function stepin() { return this._POST('stepin', undefined);}
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
hdb's stepin()
App/Control.pm: 15
__PACKAGE__->add_route( 'get', '/stepin', \&stepin);sub stepin { my($class, $env) = @_; $class->stepin; return $class->_delay_status_return($env);}
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
hdb's stepin()
Streaming response
Send status/headers.Tell Plack to exit the main loop.Set callback for next breakpoint.
App/Control.pm: 90
sub _delay_status_return { my($class, $env) = @_; return sub { my $responder = shift; my $writer = $responder->( [ 200, ['Content-Type' => 'application/json']]);
$env->{'psgix.harakiri.commit'} = TRUE;
$class->on_notify_stopped(sub { my $status = $class->_status_data(); $write->write(encode_json($status)); $writer->close; }); };}
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
hdb's stepin()
Streaming response
Leaves debugger loop
App/Control.pm: 90
sub _delay_status_return { my($class, $env) = @_; return sub { my $responder = shift; my $writer = $responder->( [ 200, ['Content-Type' => 'application/json']]);
$env->{'psgix.harakiri.commit'} = TRUE;
$class->on_notify_stopped(sub { my $status = $class->_status_data(); $write->write(encode_json($status)); $writer->close; }); };}
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
hdb's stepin()
Streaming response
Leaves debugger loop
Control returns to program
At next breakpoint, callback is invokedSends response to original request
App/Control.pm: 90
sub _delay_status_return { my($class, $env) = @_; return sub { my $responder = shift; my $writer = $responder->( [ 200, ['Content-Type' => 'application/json']]);
$env->{'psgix.harakiri.commit'} = TRUE;
$class->on_notify_stopped(sub { my $status = $class->_status_data(); $write->write(encode_json($status)); $writer->close; }); };}
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
hdb's stepin()
Streaming response
Leaves debugger loop
Control returns to program
Promise is kept
debugger.js: 301
function controlButtonClicked() { restinterface .stepin() .done(handleControlButtonResponse);}
Another request
Click event
Call stepin
Send POST request
Server receives POST
Plack encodes the request
Router middleware
hdb's stepin()
Streaming response
Leaves debugger loop
Control returns to program
Promise is kept
Update display
debugger.js: 301
function handleControlButtonResponse(rsp) {
stackManager.update() .progress(stackFrameChanged) .done(done_after_stack_update);
rsp.events.forEach(function(ev) { var event = new ProgramEvent(ev); Event.render($elt) .done(function(button) { If (button == 'exit') { Restinterface .exit() .done($elt.trigger('hangup')); }); }); }