# What `lazybones` is a small HTTP routing framework. Features include: - Different server backends. (At the moment only Hunchentoot is supported) - Modular and "installable" applications. - Handy macros for provisioning apps and defining endpoints. - Livecoding supported for your endpoint handlers and application configurations. - Automatic documentation generation for `lazybones:app` instances. Although lazybones can be used to develop and serve page-oriented web sites, it has been written to help me develop "self documenting" HTTP APIs. ## Main Components The main components in `lazybones` are the two classes: `lazybones:app` and `lazybones:endpoint`. Endpoints are objects that represent everything required to handle an HTTP request. Endpoints are collected into groups called apps. Apps, in addition to being collections of endpoints, are the main unit of development for larger lazybones projects. Apps can be installed to running servers on the fly as whole units. If desired, they can also be uninstalled. Apps are meant to be devolped in a one-app-per-package manner. That is, although you can create and work with multiple apps in a single Common Lisp package, `lazybones` encourages you to limit yourself to one per package. See the example below for more. ## A Hello World Example ``` lisp (asdf:load-system "lazybones-hunchentoot") (defpackage #:hello-lazybones (:use #:cl) (:local-nicknames (#:lzb #:lazybones)) (:import-from #:lazybones #:defendpoint* #:http-ok)) (in-package :hello-lazybones) (defendpoint* :get "/hello/:name:" () (http-ok (format nil "Welcome to Lazybones, ~a" name))) (defvar *my-server* (lzb:create-server)) (lzb:install-app *my-server* (lzb:app)) (lzb:start-server *my-server*) ``` Go ahead and test this sever out with curl: ``` shell $ curl -v http://localhost:8888/hello/colin * Trying 127.0.0.1:8888... * Connected to localhost (127.0.0.1) port 8888 (#0) > GET /hello/colin HTTP/1.1 > Host: localhost:8888 > User-Agent: curl/7.74.0 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Length: 27 < Date: Wed, 09 Feb 2022 12:26:01 GMT < Server: Hunchentoot 1.3.0 < Content-Type: text/html; charset=utf-8 < * Connection #0 to host localhost left intact Welcome to Lazybones, colin ``` ## A Showcase Example The following is quick example showing a few more things that `lazybones` can do. ``` lisp (asdf:load-system "lazybones-hunchentoot") (defpackage #:lazybones-test (:use #:cl) (:local-nicknames (#:lzb #:lazybones)) (:import-from #:lazybones #:defendpoint* #:http-ok #:http-err)) (in-package :lazybones-test) ;; first make a server and add some custom error responses (defvar *server* (lzb:create-server :port 8888)) (defun custom-404 () (format nil "~a wasn't found :(" (lzb:request-path))) ; can use request functiosn (defun custom-403 () "You, in particular, can't do that. :P ") (lzb:set-canned-response *server* 404 'custom-404 "text/plain" ) (lzb:set-canned-response *server* 403 'custom-403 "text/plain" ) (lzb:set-canned-response *server* 500 #p"/path/to/500error.txt" "text/plain" ) ;; PPROVISION-APP makes an app. You can supply an optional name, a symbol. ;; In lieu of a supplied name, the name of the package is used as the app's name. (lzb:provision-app () :title "Lazybones Demo App" :version "0.0.0" :description "Just an API that defines some endpoints. These endpoints aren't meant to accomplish anything. merely to test out the lazybones HTTP routing framework." :content-type "text/plain" ; default content type of server responses. :auth 'post-authorizer) ; default authorizor for requests that need it (defun post-authorizer () "Request is authorized if it contains the right TESTAPPSESSION cookie. Obtain such a cookie by posting to the /login endpoint." (string-equal "coolsessionbro" (lzb:request-cookie "testappsession"))) ;; now we install the app to the server (lzb:install-app *server* (lzb:app)) ; (app) is the default app for this package ;; DEFENDPOINT* is a macro to define an endpoint and install it into the ;; app whose name is the current package anme. DEFENDPOINT (without the *) ;; allows you to explictly specify the app where the endpoint is installed. (defendpoint* :post "/login" () "Dummy login endpoint for returning a session cookie. Always returns the \"true\" and sends a set-cookie header, setting 'testappsession' to 'coolsessionbro'." (print (lzb:request-body)) ; dummy implementation prints post body to stdout (setf (lzb:response-cookie "testappsession") "coolsessionbro") (http-ok "true")) (defendpoint* :get "/hello/:who:" () "Just says hello to WHO" (http-ok (format nil "Hello ~a" who))) (defendpoint* :post "/hello/:who:" (:auth t) ; use the default authorizor for the app "Post something to hello who" (let ((body (lzb:request-body))) (http-ok (format nil "Hello ~a, I got your message ~a" who body)))) (defendpoint* :get "/search" () "Echo the search parameters in a nice list." (http-ok (format nil "Query Was:~%~{~a is ~a~%~}~%" (loop for (x . y) in (lzb:request-parameters) collect x collect y)))) (defun crapshoot-authorizer () "Randomly decides that the request is authorized" (< 5 (random 10))) (defendpoint* :post "/search" (:auth 'crapshoot-authorizer) ; use custom authorizer "Echo the search parameters in a nice list, but also has a post-body" (http-ok (with-output-to-string (out) (format out "Query Was:~%~{~a is ~a~%~}~%" (loop for (x . y) in (lzb:request-parameters) collect x collect y)) (terpri) (format out "Decoded Post Body: ~s~%" (lzb:request-body))))) (defun to-int (string) "An Integer" (parse-integer string)) ;; route variables can accept parsers / preformatters ;; these will parse a value and supply it to the argument of the handler. ;; int eh following CATEGORY is an int (defendpoint* :get "/search/:category to-int:" () "Echo the search back, but in a specific category" (assert (typep category 'integer)) ; just to show you. (http-ok (format nil "Searching in ~a with parameters:~%~{~a = ~a~%~}~%" category (loop for (x . y) in (lzb:request-parameters) collect x collect y)))) (defun person-by-id (id) "A Person Instance" ;; The real thing might perform some database operation here. If the ;; operation failed, an error could be signalled, in which case a ;; 500 response would be sent to the client. (list :name "Colin" :occupation "Macrologist" :id (parse-integer id))) (defendpoint* :get "/person/:person person-by-id:" (:content-type "application/json") "Returns a json representation of the person." (http-ok (jonathan:to-json person))) ;; If you like start the server (lzb:start-server *server*) ``` ## Generate Documentation Generate a markdown file documenting the endpoints of an APP / API like so: ``` ;; continuing from above (in-package :lazybones-test) (alexandria:write-string-into-file (lzb:generate-app-documentation (lzb:app)) #p"lazybones-test-docs.md" :if-exists :supersede) ``` Then use your favorite markdown processor to generate HTML or whatever your preferred documentation distribution format is. See `example/lazybones-test-docs.md` for an example of the documentation generator output. ## Backends **WARNING** Users can mostly ignore thissection. The API for alternate backends is in flux. To implement a new backend for lazybones, consult the `lazybones.backend` package. Define a new system and a new package that `uses` the lazybones.backend package. Implement functions for each symbol. Consult the `lazybones-hunchentoot` system for an example backend.