REST from the trenches

REST from the trenches

This is a talk I gave at YAPC::NA 2012 in Madison, Wisconsin

78244476bb128a3a10522fc215bd2e83?s=128

Stevan Little

June 13, 2012
Tweet

Transcript

  1. REST from the trenches Stevan Little YAPC::NA 2012 stevan.little@iinteractive.com

  2. YAPC::NA 2011 So last year at YAPC 2011 I gave

    a talk ...
  3. In that talk I was discussing a framework I was

    working on at $work which I had given the name Jackalope.
  4. In the talk I mentioned that this it was part

    1 of 2, largely because Jackalope was a very young framework and still had “lots of assembly required” to make it work. So I punted the rest of the talk to YAPC::NA 2012, this was partially because I knew Jackalope wasn’t ready and hadn’t really lived enough in the real world, and partially because ...
  5. well to be honest, ... cause I hoped I wouldn’t

    ever have to give part 2
  6. YAPC::NA But since that didn’t happen, I guess I will

    have to make the best of it.
  7. In that talk, for those of you who weren’t there,

    I spoke about a few things, first was the state of Web-Services ...
  8. Level 3 Web Services Richardson Maturity Model Level 0 -

    throwing feces over HTTP Level 1 - well at least they are using URLs Level 2 - using HTTP verbs and response codes (this is where most people stop) Level 3 - HATEOAS
  9. HATEOAS Hypermedia As The Engine Of Application State Hateoas is

    a concept described by Roy Fielding in his thesis in which he described the REST architectural style. One key thing to keep in mind, REST is an “architectural style”, which is more akin to a design pattern, which is yet another misunderstood idea, which perhaps explains why REST is so misunderstood (trust me, I know, I misunderstood it a few times). So after spending some time on theory, I then ...
  10. Jackalope is tEh awesum!!!1! ... then I spent a bunch

    of time talking about how cool Jackalope was ...
  11. now fast forward a year and ...

  12. Things I got wrong Well, yeah, ... not so much

    ...
  13. Things I got wrong ‣ Schema Languages are bad ‣

    ‣ ‣ ‣ “Be strict in what you emit, liberal in what you accept” There are good points to this and bad points, but the reality is that if you want people to use your API, this is probably a good philosophy to follow. Schema languages are just overhead, both computationally and conceptually, and they create a tight coupling between the client and server, by being more liberal in what you accept it is easier to evolve your service.
  14. Things I got wrong ‣ Schema Languages are bad ‣

    REST != CRUD ‣ ‣ ‣ REST is often associated with CRUD, partially because the ATOM Publishing protocol is a great example of REST, and partially cause CRUD is a simple thing to map too. Reality is that not all your web-services are CRUD and so anything that makes that assumption fails. In particular Jackalope got to the point where it insisted on CRUD and anything non-CRUD had to work around it.
  15. Things I got wrong ‣ Schema Languages are bad ‣

    REST != CRUD ‣ You can’t selectively use HTTP ‣ ‣ HTTP is hard, there is a lot of stuff to think about. But if you really are intent on using it, then you should use it all, not just a handful of status codes and a few methods. A perfect example of where Jackalope got this wrong is that it stuffed metadata into the body (the ID (should have been the Location header), the version (should have been in the ETag header) and links (should have been in the Links header)). Later on I will demonstrate this and how it made certain things just work.
  16. Things I got wrong ‣ Schema Languages are bad ‣

    REST != CRUD ‣ You can’t selectively use HTTP ‣ Front-end guys hate HATEOAS ‣ HATEOAS is a great concept, I still believe in it, but not for all cases. Writing discoverable code is a really cool idea, but fact is that a stable UI needs to be able to rely on certain things. So there has to be a balance here, which means, support both.
  17. Things I got wrong ‣ Schema Languages are bad ‣

    REST != CRUD ‣ You can’t selectively use HTTP ‣ Front-end guys hate HATEOAS ‣ Content-Type = application/json JSON is great, I love it. However, when all you emit is application/json, you are missing out. Content-types are important and content-negotiation is even more so. A lot can be said through custom content-types, including the ability to “downgrade” and deal with plain old JSON.
  18. So basically it was back to the drawing board ...

    and after some thinking, I decided that for my next effort I needed to keep a couple things in mind.
  19. ‣ K.I.S.S. First it that things had to be simple.

    It should be easy to create new endpoints for my API and not have to jump through various hoops of complexity.
  20. ‣ K.I.S.S. ‣ Discoverability Next was discoverability, which is not

    just for computers, but for humans too. It should be possible to explore an API with something as low-level as Curl and to be able to navigate it as a human (albiet a human who knows HTTP), and then program a computer to do the same. The key to this I believe is actually being strict about your HTTP, which gives you a common language, which is hugely important.
  21. ‣ K.I.S.S. ‣ Discoverability ‣ Hackability And lastly hackability. I

    have long held the belief that a good architecutre has something I call a “Hack Layer” in it. This is a reasonably isolated layer in which you stuff all the bits which make no logical sense, but do make business sense (cause we all know business is not logical). So from here I started doing some research ...
  22. None
  23. ... then I saw this ...

  24. None
  25. None
  26. None
  27. None
  28. None
  29. None
  30. None
  31. Web::Machine https://github.com/stevan/webmachine-perl And so all of this lead me to

    port Web-Machine to Perl. Web-Machine is really made up of two parts, ...
  32. Web::Machine::FSM in_conflict is_authorized known_content_type known_method last_modified_is_greater_than_if_modified_since last_modified_is_greater_than_if_unmodified_since malformed_request method_allowed method_is_delete

    method_is_get_or_head method_is_options method_is_post method_is_put moved_permanently moved_temporarily multiple_choices multiple_representations new_resource previously_existed redirect request_entity_too_large resource_exists response_body_includes_entity service_available uri_too_long acceptable_media_type_available accept_charset_choice_available accept_charset_exists accept_encoding_choice_available accept_encoding_exists accept_header_exists accept_language_choice_available accept_language_header_exists allow_post_to_missing_resource content_headers_okay delete_enacted_immediately did_delete_complete etag_in_if_match_list etag_in_if_none_match forbidden if_match_exists if_match_exists_and_if_match_is_wildcard if_match_is_wildcard if_modified_since_exists if_modified_since_greater_than_now if_modified_since_is_valid_date if_none_match_exists if_none_match_is_wildcard if_unmodified_since_exists if_unmodified_since_is_valid_date The FSM has 57 states that basically ask 50 different possible questions that are asked of a resource, a request and a response to move it through the states machine.
  33. Predicates service_available is_authorized forbidden allow_missing_post malformed_request uri_too_long known_content_type valid_content_headers valid_entity_length

    delete_completed post_is_create is_conflict multiple_choices previously_existed moved_permanently moved_temporarily Informational base_uri allowed_methods known_methods charsets_provided languages_provided encodings_provided variances last_modified expires generate_etag Actions options resource_exists content_types_provided content_types_accepted delete_resource create_path process_post finish_request Web::Machine::Resource A resource has 34 methods you can override to produce functionality, I have basically grouped them into three categories here. - Predicates are (for the most part) yes/no questions which guide through the state diagram - Informational methods provide Web::Machine with information about your resource - Action methods are the places where you typically do the actual work
  34. Link Content-Type Accept Accept-Charset Accept-Encoding Accept-Language Date Client-Date Expires Last-Modified

    If-Unmodified-Since If-Modified-Since WWW-Authenticate Authentication-Info Authorization HTTP::Headers::ActionPack::LinkList HTTP::Headers::ActionPack::MediaType HTTP::Headers::ActionPack::MediaTypeList HTTP::Headers::ActionPack::PriorityList HTTP::Headers::ActionPack::PriorityList HTTP::Headers::ActionPack::PriorityList HTTP::Headers::ActionPack::DateHeader HTTP::Headers::ActionPack::DateHeader HTTP::Headers::ActionPack::DateHeader HTTP::Headers::ActionPack::DateHeader HTTP::Headers::ActionPack::DateHeader HTTP::Headers::ActionPack::DateHeader HTTP::Headers::ActionPack::WWWAuthenticate HTTP::Headers::ActionPack::AuthenticationInfo HTTP::Headers::ActionPack::Authorization HTTP::Headers::ActionPack https://github.com/stevan/http-headers-actionpack
  35. Examples

  36. use 5.16.0; package YAPC::NA::2012::Example000::Resource { use JSON::XS qw[ encode_json ];

    use parent 'Web::Machine::Resource'; sub content_types_provided { [{ 'application/json' => 'to_json' }] } sub to_json { encode_json( { message => 'Hello World' } ) } } Web::Machine->new( resource => 'YAPC::NA::2012::Example000::Resource' )->to_app; curl -v http://0:5000/ # fails with a 406 curl -v http://0:5000/ -H 'Accept: image/jpeg'
  37. Y U NO MOOSE?!?!?!

  38. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:23:58 GMT < Server: HTTP::Server::PSGI < Content-Length: 25 < Content-Type: application/json < {"message":"Hello World"}
  39. % curl -v http://0:5000/ -H 'Accept: text/html' > GET /

    HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: text/html > < HTTP/1.0 406 Not Acceptable < Date: Thu, 07 Jun 2012 01:29:55 GMT < Server: HTTP::Server::PSGI < Content-Length: 14 < Not Acceptable
  40. % curl -v -X DELETE http://0:5000/ > DELETE / HTTP/1.1

    > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 405 Method Not Allowed < Date: Sun, 10 Jun 2012 19:13:54 GMT < Server: HTTP::Server::PSGI < Allow: GET, HEAD < Content-Length: 18 < Method Not Allowed
  41. use 5.16.0; package YAPC::NA::2012::Example001::Resource { use JSON::XS qw[ encode_json ];

    use parent 'Web::Machine::Resource'; sub content_types_provided { [ { 'application/json' => 'to_json' }, { 'text/html' => 'to_html' }, ] } sub to_json { encode_json( { message => 'Hello World' } ) } sub to_html { '<html><body><h1>Hello World</h1></body></html>' } } Web::Machine->new( resource => 'YAPC::NA::2012::Example001::Resource' )->to_app; This test shows that the order of content_types_provided is actually important if you do not specify a media-type. # JSON is the default ... curl -v http://0:5000/ # you must ask specifically for HTML curl -v http://0:5000/ -H 'Accept: text/html' # but open in a browser and you get HTML open http://0:5000/
  42. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:23:58 GMT < Server: HTTP::Server::PSGI < Vary: Accept < Content-Length: 25 < Content-Type: application/json < {"message":"Hello World"} Note the Vary header here, this is important header when doing caching, it basically says “the Accept header can have variances, so be careful what you cache”.
  43. % curl -v http://0:5000/ -H 'Accept: text/html' > GET /

    HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: text/html > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:31:23 GMT < Server: HTTP::Server::PSGI < Vary: Accept < Content-Length: 46 < Content-Type: text/html < <html><body><h1>Hello World</h1></body></html>
  44. use 5.16.0; package YAPC::NA::2012::Example002::Resource { use JSON::XS qw[ encode_json ];

    use parent 'Web::Machine::Resource'; sub content_types_provided { [ { 'text/html' => 'to_html' }, { 'application/json' => 'to_json' }, ] } sub to_json { encode_json( { message => 'Hello World' } ) } sub to_html { '<html><body><h1>Hello World</h1></body></html>' } } Web::Machine->new( resource => 'YAPC::NA::2012::Example002::Resource' )->to_app; And showing preference is just as simple as changing the order of items in content_types_provided # now HTML is the default curl -v http://0:5000/ # and you must ask specifically for JSON curl -v http://0:5000/ -H 'Accept: application/json'
  45. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:31:23 GMT < Server: HTTP::Server::PSGI < Vary: Accept < Content-Length: 46 < Content-Type: text/html < <html><body><h1>Hello World</h1></body></html>
  46. % curl -v http://0:5000/ -H 'Accept: application/json' > GET /

    HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: application/json > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:23:58 GMT < Server: HTTP::Server::PSGI < Vary: Accept < Content-Length: 25 < Content-Type: application/json < {"message":"Hello World"}
  47. use 5.16.0; package YAPC::NA::2012::Example010::Resource { use JSON::XS (); use parent

    'Web::Machine::Resource'; sub content_types_provided { [{ 'application/json' => 'to_json' }] } sub to_json { my $self = shift; JSON::XS->new->pretty->encode([ map { +{ $_->[0] => $_->[1]->type } } $self->request->header('Accept')->iterable ]) } } Web::Machine->new( resource => 'YAPC::NA::2012::Example010::Resource' )->to_app; Curl by default, it accepts anything, as you can see when we run this. curl -v http://0:5000/ However, web browsers are more sophisticated creatures and have more complicated needs. open http://0:5000/ You can see that since we only provide JSON, that we end up matching the */* at the end.
  48. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:59:15 GMT < Server: HTTP::Server::PSGI < Content-Length: 32 < Content-Type: application/json < [ { "1" : "*/*" } ]
  49. None
  50. use 5.16.0; package YAPC::NA::2012::Example011::Resource { use JSON::XS (); use parent

    'Web::Machine::Resource'; sub content_types_provided { [ { 'application/json' => 'to_json' }, { 'text/html' => 'to_html' } ] } sub to_json { ... } sub to_html { my $self = shift; '<html><body><ul>' . (join "" => map { '<li>' . $_->[0] . ' &mdash; ' . $_->[1]->type . '</li>' } $self->request->header('Accept')->iterable) . '</ul></body></html>' } } Web::Machine->new( resource => 'YAPC::NA::2012::Example011::Resource' )->to_app; So what happens then if we provide HTML as well? open http://0:5000/ Now we prefer HTML over JSON, even though JSON is the default here. If you call curl, you get the expected JSON. curl -v http://0:5000/
  51. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 01:59:15 GMT < Server: HTTP::Server::PSGI < Content-Length: 32 < Content-Type: application/json < [ { "1" : "*/*" } ]
  52. None
  53. use 5.16.0; package YAPC::NA::2012::Example012::Resource { use GD::Simple; use parent 'Web::Machine::Resource';

    sub content_types_provided { [ { 'image/gif' => 'to_gif' }, { 'text/html' => 'to_html' }, ] } sub to_html { '<html><body><ul>' . (join "" => map { '<li>' . $_->[0] . ' &mdash; ' . $_->[1]->type . '</li>' } (shift)->request->header('Accept')->iterable) . '</ul><br/><img src="/hello_world.gif" border="1"/></body></html>' } sub to_gif { my $img = GD::Simple->new( 130, 20 ); $img->fgcolor('red'); $img->moveTo(15, 15); $img->string( (shift)->request->path_info ); $img->gif; } } Web::Machine->new( resource => 'YAPC::NA::2012::Example012::Resource' )->to_app; And of course, you don't have to just provide text based results ...
  54. None
  55. None
  56. use 5.16.0; package YAPC::NA::2012::Example020::Resource { use Web::Machine::Util qw[ create_header ];

    use parent 'Web::Machine::Resource'; sub content_types_provided { [ { 'text/html' => 'to_html' } ] } sub to_html { '<html><body><h1>Hello World</h1></body></html>' } sub is_authorized { my ($self, $auth_header) = @_; if ( $auth_header ) { return 1 if $auth_header->username eq 'foo' && $auth_header->password eq 'bar'; } return create_header( 'WWWAuthenticate' => [ 'Basic' => ( realm => 'Webmachine' ) ] ); } } Web::Machine->new( resource => 'YAPC::NA::2012::Example020::Resource' )->to_app;
  57. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 401 Unauthorized < Date: Thu, 07 Jun 2012 02:22:42 GMT < Server: HTTP::Server::PSGI < WWW-Authenticate: Basic realm="Webmachine" < Content-Length: 12 < Unauthorized
  58. % curl -v http://0:5000/ -H 'Authorization: Basic Zm9vOmJhcg==' > GET

    / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > Authorization: Basic Zm9vOmJhcg== > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 02:25:31 GMT < Server: HTTP::Server::PSGI < Content-Length: 46 < Content-Type: text/html < <html><body><h1>Hello World</h1></body></html>
  59. % curl -v http://0:5000/ -H 'Authorization: Basic ZOMGBBQLOL==' > GET

    / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > Authorization: Basic ZOMGBBQLOL== > < HTTP/1.0 401 Unauthorized < Date: Sun, 10 Jun 2012 19:26:31 GMT < Server: HTTP::Server::PSGI < WWW-Authenticate: Basic realm="Webmachine" < Content-Length: 12 < Unauthorized
  60. use 5.16.0; package YAPC::NA::2012::Example030::Resource { use parent 'Web::Machine::Resource'; sub save_message

    { ... } sub get_messages { ... } sub allowed_methods { [qw[ GET POST ]] } sub content_types_provided { [ { 'text/html' => 'to_html' } ] } sub to_html { my $self = shift; '<html><body><form method="POST"><input type="text" name="message" />' . '<input type="submit" /></form><hr/><ul>' . (join '' => map { '<li>' . $_ . '</li>' } $self->get_messages) . '</ul></body></html>' } sub process_post { my $self = shift; $self->save_message( $self->request->param('message') ); $self->response->header('Location' => '/'); return \301; } } Web::Machine->new( resource => 'YAPC::NA::2012::Example030::Resource' )->to_app;
  61. None
  62. None
  63. None
  64. use 5.16.0; package YAPC::NA::2012::Example031::Resource { use JSON::XS (); use parent

    'YAPC::NA::2012::Example030::Resource'; sub allowed_methods { [qw[ GET PUT POST ]] } sub content_types_accepted { [ { 'application/json' => 'from_json' } ] } sub from_json { my $self = shift; $self->save_message( JSON::XS->new->allow_nonref->decode( $self->request->content ) ); } sub process_post { my $self = shift; return \415 unless $self->request->header('Content-Type') ->match('application/x-www-form-urlencoded'); $self->SUPER::process_post; } } Web::Machine->new( resource => 'YAPC::NA::2012::Example031::Resource' )->to_app;
  65. % curl -v -X PUT http://0:5000/ -H 'Content-Type: application/json' -d

    '"bar"' > PUT / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > Content-Type: application/json > Content-Length: 5 > < HTTP/1.0 204 No Content < Date: Sun, 10 Jun 2012 19:36:49 GMT < Server: HTTP::Server::PSGI < Content-Type: text/html <
  66. None
  67. % curl -v -X POST http://0:5000/ -H 'Content-Type: application/json' -d

    '"bar"' > POST / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > Content-Type: application/json > Content-Length: 5 > < HTTP/1.0 415 Unsupported Media Type < Date: Sun, 10 Jun 2012 19:49:21 GMT < Server: HTTP::Server::PSGI < Content-Type: text/html < Content-Length: 22 < Unsupported Media Type
  68. use 5.16.0; package YAPC::NA::2012::Example032::Resource { use Web::Machine::Util qw[ create_header ];

    use parent 'YAPC::NA::2012::Example031::Resource'; sub is_authorized { my ($self, $auth_header) = @_; return 1 if $self->request->method ne 'PUT'; if ( $auth_header ) { return 1 if $auth_header->username eq 'foo' && $auth_header->password eq 'bar'; } return create_header( 'WWWAuthenticate' => [ 'Basic' => ( realm => 'Webmachine' ) ] ); } } Web::Machine->new( resource => 'YAPC::NA::2012::Example032::Resource' )->to_app;
  69. % curl -v -X PUT http://0:5000/ -H 'Content-Type: application/json' -d

    '"baz"' > PUT / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > Content-Type: application/json > Content-Length: 5 > < HTTP/1.0 401 Unauthorized < Date: Sun, 10 Jun 2012 20:07:57 GMT < Server: HTTP::Server::PSGI < WWW-Authenticate: Basic realm="Webmachine" < Content-Length: 12 < Unauthorized
  70. % curl -v -X PUT http://0:5000/ -H 'Content-Type: application/json' -H

    'Authorization: Zm9vOmJhcg==' -d '"baz"' > PUT / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > Content-Type: application/json > Authorization: Basic Zm9vOmJhcg== > Content-Length: 5 > < HTTP/1.0 204 No Content < Date: Sun, 10 Jun 2012 20:08:24 GMT < Server: HTTP::Server::PSGI < Location: / < Content-Type: text/html < Content-Length: 0 <
  71. None
  72. use 5.16.0; package YAPC::NA::2012::Example033::Resource { use JSON::XS qw[ encode_json ];

    use Web::Machine::Util qw[ create_header ]; use parent 'YAPC::NA::2012::Example032::Resource'; sub content_types_provided { my $self = shift; my $types = $self->SUPER::content_types_provided; push @$types => { 'application/json' => 'to_json' }; $types; } sub to_json { my $self = shift; $self->response->header( 'Link' => create_header( 'LinkHeader' => [ '/', ('content-type' => 'text/html') ] ) ); encode_json([ $self->get_messages ]); } } Web::Machine->new( resource => 'YAPC::NA::2012::Example033::Resource' )->to_app;
  73. None
  74. % curl -v http://0:5000 -H 'Accept: application/json' > GET /

    HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: application/json > < HTTP/1.0 200 OK < Date: Sun, 10 Jun 2012 21:44:11 GMT < Server: HTTP::Server::PSGI < Vary: Accept < Content-Length: 19 < Content-Type: application/json < Link: </>; content-type="text/html" < ["foo","bar","baz"]
  75. use 5.16.0; package YAPC::NA::2012::Example100::Resource { use Web::Machine::Util qw[ create_date ];

    use parent 'Web::Machine::Resource'; sub content_types_provided { [{ 'text/html' => 'to_html' }] } sub last_modified { create_date('Sun, 27 May 2012 17:35:00 EDT') } sub generate_etag { '0xDEADBEEF' } sub to_html { '<html><body><h1>Hello World</h1></body></html>' } } Web::Machine->new( resource => 'YAPC::NA::2012::Example100::Resource' )->to_app; curl -v http://0:5000/ -H 'If-Modified-Since: Sun, 27 May 2012 17:34:59 EDT' curl -v http://0:5000/ -H 'If-Modified-Since: Sun, 27 May 2012 17:35:00 EDT'
  76. % curl -v http://0:5000/ -H 'If-Modified-Since: Sun, 27 May 2012

    17:34:59 EDT' > GET / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > If-Modified-Since: Sun, 27 May 2012 17:34:59 EDT > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 02:28:55 GMT < Server: HTTP::Server::PSGI < ETag: "0xDEADBEEF" < Content-Length: 46 < Content-Type: text/html < Last-Modified: Sun, 27 May 2012 21:35:00 GMT < <html><body><h1>Hello World</h1></body></html>
  77. % curl -v http://0:5000/ -H 'If-Modified-Since: Sun, 27 May 2012

    17:35:00 EDT' > GET / HTTP/1.1 > User-Agent: curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > If-Modified-Since: Sun, 27 May 2012 17:35:00 EDT > < HTTP/1.0 304 Not Modified < Date: Thu, 07 Jun 2012 02:30:02 GMT < Server: HTTP::Server::PSGI < ETag: "0xDEADBEEF" < Last-Modified: Sun, 27 May 2012 21:35:00 GMT <
  78. use 5.16.0; package YAPC::NA::2012::Example110::Resource { use parent 'Web::Machine::Resource'; sub content_types_provided

    { [ { 'text/html' => 'to_html' }, ] } sub to_html { '<html><body><h1>Hello World</h1></body></html>' } sub service_available { my $self = shift; return 1 unless -e './site_down'; $self->response->body([ '<html><body><h1>Service Unavailable</h1></body></html>' ]); 0; } } Web::Machine->new( resource => 'YAPC::NA::2012::Example110::Resource' )->to_app; This demostrates how you can easily handle situations like the site being down in a reasonably elegant way. touch site_down rm site_down
  79. None
  80. % touch site_down

  81. None
  82. use 5.16.0; package YAPC::NA::2012::Example120::Resource { use Web::Machine::Util qw[ bind_path ];

    use parent 'Web::Machine::Resource'; sub content_types_provided { [{ 'text/html' => 'to_html' }] } sub to_html { my $self = shift; if ( my ($action, $id) = bind_path('/:action/:id', $self->request->path_info) ) { return "<html><body><h1>action('$action') id('$id')</h1></body></html>"; } else { return \404; } } } Web::Machine->new( resource => 'YAPC::NA::2012::Example120::Resource' )->to_app; curl -v http://0:5000/ curl -v http://0:5000/edit/100
  83. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 404 Not Found < Date: Thu, 07 Jun 2012 02:33:19 GMT < Server: HTTP::Server::PSGI < Content-Type: text/html < Content-Length: 9 < Not Found
  84. % curl -v http://0:5000/edit/100 > GET /edit/100 HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 02:34:00 GMT < Server: HTTP::Server::PSGI < Content-Length: 59 < Content-Type: text/html < <html><body>action('edit') id('100')</body></html>
  85. use 5.16.0; package YAPC::NA::2012::Example130::Resource { use parent 'Web::Machine::Resource'; sub content_types_provided

    { [ { 'text/html' => 'to_html' } ] } sub to_html { '<html><body><h1>Hello World</h1></body></html>' } } Web::Machine->new( resource => 'YAPC::NA::2012::Example130::Resource', tracing => 1 )->to_app;
  86. % curl -v http://0:5000/ > GET / HTTP/1.1 > User-Agent:

    curl/7.21.4 (universal-apple-darwin11.0) ... > Host: 0:5000 > Accept: */* > < HTTP/1.0 200 OK < Date: Thu, 07 Jun 2012 02:35:17 GMT < Server: HTTP::Server::PSGI < Content-Length: 46 < Content-Type: text/html < X-Web-Machine-Trace: b13,b12,b11,b10,b9,b8,b7,b6,b5,b4,b3,c3,c4,d4,e5,f6,g7,g8, h10,i12,l13,m16,n16,o16,o18,o18b < <html><body><h1>Hello World</h1></body></html>
  87. None
  88. % export WM_DEBUG=1; % plackup examples/yapc-talk-examples/130-tracing-header.psgi HTTP::Server::PSGI: Accepting connections at

    http://0:5000/ entering b13 (service_available) -> transitioning to b12 entering b12 (known_method) -> transitioning to b11 entering b11 (uri_too_long) -> transitioning to b10 entering b10 (method_allowed) -> transitioning to b9 entering b9 (malformed_request) -> transitioning to b8 entering b8 (is_authorized) -> transitioning to b7 entering b7 (forbidden) -> transitioning to b6 entering b6 (content_headers_okay) -> transitioning to b5 entering b5 (known_content_type) -> transitioning to b4 entering b4 (request_entity_too_large) -> transitioning to b3 entering b3 (method_is_options) -> transitioning to c3 entering c3 (accept_header_exists) -> transitioning to c4 entering c4 (acceptable_media_type_available) -> transitioning to d4 entering d4 (accept_language_header_exists) -> transitioning to e5 entering e5 (accept_charset_exists) -> transitioning to f6
  89. entering o18 (multiple_representations) -> transitioning to o18b entering o18b (multiple_choices)

    .. terminating with 200 $VAR1 = { 'psgi.multiprocess' => '', 'SCRIPT_NAME' => '', 'SERVER_NAME' => 0, 'PATH_INFO' => '/', 'HTTP_ACCEPT' => '*/*', 'REQUEST_METHOD' => 'GET', 'psgi.multithread' => '', 'HTTP_USER_AGENT' => 'curl/7.21.4 (universal-apple- darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5', 'QUERY_STRING' => '', 'SERVER_PORT' => 5000, 'psgix.input.buffered' => 1, 'REMOTE_ADDR' => '127.0.0.1', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'psgi.streaming' => 1, 'psgi.errors' => *::STDERR, 'REQUEST_URI' => '/', 'psgi.version' => [ 1, 1 ], 'psgi.nonblocking' => '', 'psgix.io' => bless( \*Symbol::GEN1, 'IO::Socket::INET' ), 'psgi.url_scheme' => 'http', 'psgi.run_once' => '', 'HTTP_HOST' => '0:5000', 'psgi.input' => \*{'HTTP::Server::PSGI::$input'} }; $VAR1 = [
  90. 'psgi.run_once' => '', 'HTTP_HOST' => '0:5000', 'psgi.input' => \*{'HTTP::Server::PSGI::$input'} };

    $VAR1 = [ 200, [ 'Content-Length', 46, 'Content-Type', 'text/html' ], [ '<html><body><h1>Hello World</h1></body></html>' ] ]; 127.0.0.1 - - [10/Jun/2012:16:21:59 -0400] "GET / HTTP/1.1" 200 46 "-" "curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5"
  91. Conclusions ‣ ‣ ‣

  92. Conclusions ‣ The Web is messy ‣ ‣ The reality

    is that the web is a very messy place, it is important that your web-service can live in this world. The best way to deal with this, is to use HTTP to it’s fullest, it is the common language. This increases discoverability too, and is just an all around good idea.
  93. Conclusions ‣ The Web is messy ‣ Use REST toolkits

    not frameworks ‣ Frameworks are basically a big ball of opinion with a few bits left undone for you to complete. REST does not lend itself to this, instead I recommend using a Toolkit, meaning, a set of libraries with which you accomplish REST.
  94. Conclusions ‣ The Web is messy ‣ Use REST toolkits

    not frameworks ‣ Simple and Direct Web services often layer over complex systems, therefore keeping the web-layer simple is a good thing.
  95. The End

  96. Questions?