{"id":26124423,"url":"https://github.com/dnaeon/cl-jingle","last_synced_at":"2025-03-10T16:08:19.942Z","repository":{"id":64603885,"uuid":"576316184","full_name":"dnaeon/cl-jingle","owner":"dnaeon","description":"Common Lisp web framework with bells and whistles (based on ningle)","archived":false,"fork":false,"pushed_at":"2024-12-27T07:49:36.000Z","size":3222,"stargazers_count":48,"open_issues_count":1,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-12-27T08:29:21.430Z","etag":null,"topics":["common-lisp","lisp","web","webdev","webframework"],"latest_commit_sha":null,"homepage":"","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dnaeon.png","metadata":{"files":{"readme":"README.org","changelog":"CHANGELOG.org","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["dnaeon"]}},"created_at":"2022-12-09T14:37:08.000Z","updated_at":"2024-12-27T07:49:40.000Z","dependencies_parsed_at":"2024-06-21T16:49:35.728Z","dependency_job_id":"19147607-3c82-4c15-8706-514d49ca607f","html_url":"https://github.com/dnaeon/cl-jingle","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dnaeon%2Fcl-jingle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dnaeon%2Fcl-jingle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dnaeon%2Fcl-jingle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dnaeon%2Fcl-jingle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dnaeon","download_url":"https://codeload.github.com/dnaeon/cl-jingle/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242883681,"owners_count":20200979,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["common-lisp","lisp","web","webdev","webframework"],"created_at":"2025-03-10T16:08:19.045Z","updated_at":"2025-03-10T16:08:19.893Z","avatar_url":"https://github.com/dnaeon.png","language":"Common Lisp","funding_links":["https://github.com/sponsors/dnaeon"],"categories":["Interfaces to other package managers"],"sub_categories":["Clack plugins"],"readme":"* jingle\n\n=jingle= is [[https://github.com/fukamachi/ningle][ningle]], but with bells and whistles.\n\n* Requirements\n\n- [[https://www.quicklisp.org/beta/][Quicklisp]]\n\n* Installation\n\n=jingle= is not yet in Quicklisp, so in order to install it you will need to\nclone the repo and add it to your [[https://www.quicklisp.org/beta/faq.html][Quicklisp local-projects]].\n\n#+begin_src shell\n  cd ~/quicklisp/local-projects\n  git clone https://github.com/dnaeon/cl-jingle.git\n#+end_src\n\n* Demo\n\nThe =JINGLE.DEMO= system provides a ready-to-run example REST API,\nwhich comes with an [[https://swagger.io/specification/][OpenAPI 3.x spec]], [[https://swagger.io/tools/swagger-ui/][Swagger UI]], and a command-line\ninterface app built with [[https://github.com/dnaeon/clingon][clingon]].\n\nIn order to build the demo application, simply execute this command.\n\n#+begin_src shell\n  make demo\n#+end_src\n\nThis will build the =jingle-demo= app, which you can find in the\n=bin/= directory.\n\nIn order to start the HTTP server, execute the following command.\n\n#+begin_src shell\n  bin/jingle-demo serve\n#+end_src\n\nOnce the HTTP server is up and running navigate to\n[[http://localhost:5000/api/docs/][http://localhost:5000/api/docs/]] which should take you to the Swagger\nUI.\n\n[[./images/jingle-swagger-ui.png]]\n\nMake sure to check the various sub-commands provided by =jingle-demo=,\nwhich allow you to interface with the REST APIs.\n\nYou can also run the demo app in Docker. First, build the image.\n\n#+begin_src shell\n  make demo-docker\n#+end_src\n\nOnce the image is built you can start up the service by executing the\nfollowing command.\n\n#+begin_src shell\n   docker run -p 5000:5000 cl-jingle:latest serve --address 0.0.0.0\n#+end_src\n\nYou can also view a short demo of the command-line interface\napplication, which interfaces with the REST API here.\n\n[[./images/jingle-demo.gif]]\n\n* Usage\n\nStart up your REPL and load the =JINGLE= system.\n\n#+begin_src lisp\n  CL-USER\u003e (ql:quickload :jingle)\n  To load \"jingle\":\n    Load 1 ASDF system:\n      jingle\n  ; Loading \"jingle\"\n  ..................................................\n  [package jingle.core].............................\n  [package jingle]\n\n  (:JINGLE)\n#+end_src\n\nFirst thing we need to do is to create a new =JINGLE:APP= instance.\n\n#+begin_src lisp\n  CL-USER\u003e (defparameter *app* (jingle:make-app))\n  *APP*\n#+end_src\n\nWhen creating a new instance of =JINGLE:APP= you can provide\nadditional keyword args, which specify what HTTP server to use,\naddress to bind to, the port to listen on, middlewares, etc..\n\nA very simple HTTP handler, which returns =Hello, World!= looks like\nthis.\n\n#+begin_src lisp\n  (defun hello-handler (params)\n    (declare (ignore params))\n    \"Hello, World!\")\n#+end_src\n\nThe following is an example of an HTTP handler which echoes back the\npayload you send to it.\n\n#+begin_src lisp\n  (defun echo-handler (params)\n    \"A simple handler which echoes back the payload you send to it\"\n    (declare (ignore params))\n    (jingle:set-response-header :content-type (jingle:request-content-type jingle:*request*))\n    (jingle:set-response-header :content-length (jingle:request-content-length jingle:*request*))\n    (maphash (lambda (k v)\n               (jingle:set-response-header k v))\n             (jingle:request-headers jingle:*request*))\n    (jingle:request-content jingle:*request*))\n#+end_src\n\nNext thing we need to do is register our handlers.\n\n#+begin_src lisp\n  CL-USER\u003e (setf (jingle:route *app* \"/hello\") #'hello-handler)\n  CL-USER\u003e (setf (jingle:route *app* \"/echo\" :method :post) #'echo-handler)\n#+end_src\n\nAnd now we can start the app.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:start *app*)\n#+end_src\n\nTrying out our endpoints using =curl(1)= gives us this result.\n\n#+begin_src shell\n  $ curl -vvv --get http://localhost:5000/hello\n  *   Trying 127.0.0.1:5000...\n  * Connected to localhost (127.0.0.1) port 5000 (#0)\n  \u003e GET /hello HTTP/1.1\n  \u003e Host: localhost:5000\n  \u003e User-Agent: curl/7.86.0\n  \u003e Accept: */*\n  \u003e \n  * Mark bundle as not supporting multiuse\n  \u003c HTTP/1.1 200 OK\n  \u003c Date: Fri, 09 Dec 2022 09:46:13 GMT\n  \u003c Server: Hunchentoot 1.3.0\n  \u003c Transfer-Encoding: chunked\n  \u003c Content-Type: text/html; charset=utf-8\n  \u003c \n  * Connection #0 to host localhost left intact\n  Hello, World!\n#+end_src\n\nAnd this is our echo handler.\n\n#+begin_src shell\n  $ curl -v -s --data '{\"foo\": \"bar\", \"baz\": \"42\"}' -H \"My-Header: SomeValue\" -H \"Content-Type: application/json\" -X POST http://localhost:5000/echo\n  *   Trying 127.0.0.1:5000...\n  * Connected to localhost (127.0.0.1) port 5000 (#0)\n  \u003e POST /echo HTTP/1.1\n  \u003e Host: localhost:5000\n  \u003e User-Agent: curl/7.86.0\n  \u003e Accept: */*\n  \u003e My-Header: SomeValue\n  \u003e Content-Type: application/json\n  \u003e Content-Length: 27\n  \u003e \n  * Mark bundle as not supporting multiuse\n  \u003c HTTP/1.1 200 OK\n  \u003c Date: Fri, 09 Dec 2022 13:57:30 GMT\n  \u003c Server: Hunchentoot 1.3.0\n  \u003c My-Header: SomeValue\n  \u003c Accept: */*\n  \u003c User-Agent: curl/7.86.0\n  \u003c Host: localhost:5000\n  \u003c Content-Length: 27\n  \u003c Content-Type: application/json\n  \u003c \n  * Connection #0 to host localhost left intact\n  {\"foo\": \"bar\", \"baz\": \"42\"}\n#+end_src\n\nIn order to stop the application, evaluate the following expression.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:stop *app*)\n#+end_src\n\n** Handlers\n\nHandlers are regular [[https://github.com/fukamachi/ningle][ningle]] routes, which accept a single argument,\nrepresenting the request parameters.\n\n** Environment\n\n=jingle= exports the special variable =JINGLE:*ENV*= which is\ndynamically bound to the request environment of [[https://github.com/fukamachi/lack][Lack]]. You can query\nthe environment directly from =jingle= and don't have to worry about\nwhere the environment is coming from.\n\n** Headers\n\n=jingle= provides the =JINGLE:SET-RESPONSE-HEADER= function for\nsetting up HTTP response headers.\n\nA simple handler which sets the =Content-Type= header to =text/plain=\nlooks like this.\n\n#+begin_src lisp\n  (defun hello (params)\n    (declare (ignore params))\n    (jingle:set-response-header :content-type \"text/plain\")\n    \"Hello, World!\")\n#+end_src\n\nOther useful functions which operate on HTTP headers are\n=JINGLE:GET-REQUEST-HEADER= and =JINGLE:GET-RESPONSE-HEADER=, which\nretrieve the value of the HTTP header associated with the request and\nresponse respectively.\n\n** Status Codes\n\nThe =JINGLE:SET-RESPONSE-STATUS= function sets the Status Code for the\nHTTP Response.\n\n#+begin_src lisp\n  (defun foo-handler (params)\n    (declare (ignore params))\n    (jingle:set-response-status :accepted)\n    \"Task accepted\")\n#+end_src\n\nArguments passed to =JINGLE:SET-RESPONSE-STATUS= may be a number\n(e.g. =400=), a keyword (e.g. =:bad-request=), or a string (e.g. =Bad\nRequest=) of the status code. The following three expressions are\nequivalent, and they all set the HTTP Status Code to =400 (Bad\nRequest)=.\n\n#+begin_src lisp\n  (jingle:set-response-status 400)\n  (jingle:set-response-status :bad-request)\n  (jingle:set-response-status \"Bad Request\")\n#+end_src\n\nAnother useful function operating on HTTP Status Codes is\n=JINGLE:EXPLAIN-STATUS-CODE=.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:explain-status-code 400)\n  \"Bad Request\"\n  CL-USER\u003e (jingle:explain-status-code :bad-request)\n  \"Bad Request\"\n#+end_src\n\n=JINGLE:STATUS-CODE-KIND= returns the kind of the HTTP Status Code as\nclassified by [[https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml][IANA]], e.g.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:status-code-kind 400)\n  :CLIENT-ERROR\n  CL-USER\u003e (jingle:status-code-kind :unauthorized)\n  :CLIENT-ERROR\n  CL-USER\u003e (jingle:status-code-kind :internal-server-error)\n  :SERVER-ERROR\n  CL-USER\u003e (jingle:status-code-kind :moved-permanently)\n  :REDIRECTION\n  CL-USER\u003e (jingle:status-code-kind 100)\n  :INFORMATIONAL\n  CL-USER\u003e (jingle:status-code-kind \"Accepted\")\n  :SUCCESS\n#+end_src\n\nOther HTTP status code predicates you may find useful are\n=JINGLE:INFORMATIONAL-CODE-P=, =JINGLE:SUCCESS-CODE-P=,\n=JINGLE:REDIRECTION-CODE-P=, =JINGLE:CLIENT-ERROR-CODE-P= and\n=JINGLE:SERVER-ERROR-CODE-P=.\n\n** Static Resources\n\nStatic resources can be served by adding them using\n=JINGLE:STATIC-PATH= method, e.g.\n\n#+begin_src lisp\n  (jingle:static-path *app* \"/static/\" \"~/public_html/\")\n#+end_src\n\nYou can serve static resources from multiple directories as well. In\norder to do that simply install them, before you start up the app.\n\n#+begin_src lisp\n  (jingle:static-path *app* \"/static-1/\" \"/path/to/static-1/\")\n  (jingle:static-path *app* \"/static-2/\" \"/path/to/static-2/\")\n  (jingle:static-path *app* \"/static-3/\" \"/path/to/static-3/\")\n#+end_src\n\n** Directory Browser\n\nThe =JINGLE:SERVE-DIRECTORY= method installs a middleware which allows\nyou to browse the contents of a given path. For example the following\ncode exposes the =~/Documents= and =~/Projects= directories.\n\n#+begin_src lisp\n  (jingle:serve-directory *app* \"/docs\" \"~/Documents\")\n  (jingle:serve-directory *app* \"/projects\" \"~/Projects\")\n#+end_src\n\nWhen accessing the directories from the browser make sure to add a\nslash at the end of the paths. For example the above directories will\nhave to accessed at http://localhost:5000/docs/ and\nhttp://localhost:5000/projects/ respectively, if you are using the\ndefault HTTP port when starting up the app.\n\n** Middlewares\n\nYou can use regular [[https://github.com/fukamachi/lack#middlewares][Lack middlewares]] with =jingle= as well. Simply\ninstall them using the =JINGLE:INSTALL-MIDDLEWARE= method.\n\nThe following simple middleware pushes a new property to the request\nenvironment, which can be queried by the HTTP handlers.\n\nFirst, implement the middleware.\n\n#+begin_src lisp\n  (defun my-middleware (app)\n    \"A custom middleware which pushes a new property to the request\n  environment and exposes it to HTTP handlers.\"\n    (lambda (env)\n      (setf (getf env :my-middleware/message) \"my middleware message\")\n      (funcall app env)))\n#+end_src\n\nThen we create a =JINGLE:APP= and install it.\n\n#+begin_src lisp\n  CL-USER\u003e (defparameter *app* (jingle:make-app))\n  CL-USER\u003e (jingle:install-middleware *app* #'my-middleware)\n#+end_src\n\nAn example handler which uses the message placed by our middleware may\nlook like this.\n\n#+begin_src lisp\n  (defun my-handler (params)\n    (declare (ignore params))\n    (jingle:set-response-status :ok)\n    (jingle:set-response-header :content-type \"text/plain\")\n    (getf jingle:*env* :my-middleware/message))\n#+end_src\n\nFinally we have to register our handler and start the app.\n\n#+begin_src lisp\n  CL-USER\u003e (setf (jingle:route *app* \"/my-middleware\") #'my-handler)\n  CL-USER\u003e (jingle:start *app*)\n#+end_src\n\nTrying it out using =curl(1)= returns the following response.\n\n#+begin_src shell\n  $ curl -vvv --get http://localhost:5000/my-middleware\n  *   Trying 127.0.0.1:5000...\n  * Connected to localhost (127.0.0.1) port 5000 (#0)\n  \u003e GET /my-middleware HTTP/1.1\n  \u003e Host: localhost:5000\n  \u003e User-Agent: curl/7.86.0\n  \u003e Accept: */*\n  \u003e \n  * Mark bundle as not supporting multiuse\n  \u003c HTTP/1.1 200 OK\n  \u003c Date: Fri, 09 Dec 2022 11:42:17 GMT\n  \u003c Server: Hunchentoot 1.3.0\n  \u003c Transfer-Encoding: chunked\n  \u003c Content-Type: text/plain\n  \u003c \n  * Connection #0 to host localhost left intact\n  my middleware message\n#+end_src\n\nHere's an example which uses Lack's =accesslog= middleware and how to\nuse it with =jingle=. First, load the respective system, which\nprovides the middleware, and then simply install it into the =jingle=\napp.\n\n#+begin_src lisp\n  CL-USER\u003e (ql:quickload :lack-middleware-accesslog)\n  CL-USER\u003e (jingle:install-middleware *app* lack.middleware.accesslog:*lack-middleware-accesslog*)\n#+end_src\n\nSearch for other middlewares you can already use in Quicklisp, e.g.\n\n#+begin_src lisp\n  CL-USER\u003e (ql:system-apropos \"lack-middleware\")\n#+end_src\n\nYou can use middlewares to push metadata into the environment for HTTP\nhandlers to use. For example, if your HTTP handlers need to read from\nand write to a database, you may want to create a middleware, which\npushes a =CL-DBI= connection into the environment, so that HTTP\nhandlers can use it, when needed.\n\nIn order to clear out all installed middlewares you can use the\n=JINGLE:CLEAR-MIDDLEWARES= method, e.g.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:clear-middlewares *app*)\n#+end_src\n\n** Redirects\n\nRedirects in =jingle= are handled by the =JINGLE:REDIRECT= function.\n\nAn example HTTP handler which redirects to [[https://lispcookbook.github.io/cl-cookbook/][The Common Lisp Cookbook]]\nlooks like this.\n\n#+begin_src lisp\n  (defun to-the-cookbook (params)\n    (declare (ignore params))\n    (jingle:redirect \"https://lispcookbook.github.io/cl-cookbook/\"))\n#+end_src\n\nRegister the HTTP handler and start the app.\n\n#+begin_src lisp\n  CL-USER\u003e (setf (jingle:route *app* \"/cookbook\") #'to-the-cookbook)\n  CL-USER\u003e (jingle:start *app*)\n#+end_src\n\nNavigate to http://localhost:5000/cookbook and you will be\nautomatically redirected.\n\nThere is also another way for defining redirects using\n=JINGLE:REDIRECT-ROUTE=. The following example shows how to install\ntwo redirect routes to your =jingle= app, without having to\nexplicitely define the HTTP handlers in advance.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:redirect-route *app* \"/sbcl\" \"https://sbcl.org/\")\n  CL-USER\u003e (jingle:redirect-route *app* \"/ecl\" \"https://ecl.common-lisp.dev/\")\n#+end_src\n\n** Request Parameters\n\nThe =JINGLE:GET-REQUEST-PARAM= function may be used within HTTP\nhandlers to get the value associated with a given parameter.\n\nSuppose we have the following example HTTP handler, which returns\ninformation about supported products and is exposed via the\n=/api/v1/product/:name= endpoint.\n\n#+begin_src lisp\n  (defparameter *products*\n    '((:|id| 1 :|name| \"foo\")\n      (:|id| 2 :|name| \"bar\")\n      (:|id| 3 :|name| \"baz\")\n      (:|id| 4 :|name| \"qux\")\n      (:|id| 5 :|name| \"foo v2\")\n      (:|id| 6 :|name| \"bar v3\")\n      (:|id| 7 :|name| \"baz v4\")\n      (:|id| 8 :|name| \"qux v5\"))\n    \"The list of our supported products\")\n\n  (defun find-product-by-name (name)\n    \"Finds a product by name\"\n    (find name\n          *products*\n          :key (lambda (item) (getf item :|name|))\n          :test #'string=))\n\n  (defun product-handler (params)\n    \"Handles requests for /api/v1/product/:name endpoint\"\n    (jingle:set-response-status :ok)\n    (jingle:set-response-header :content-type \"application/json\")\n    (let* ((name (jingle:get-request-param params :name))\n           (product (find-product-by-name name)))\n      (if product\n          (jonathan:to-json product)\n          (progn\n            (jingle:set-response-status :not-found)\n            (jonathan:to-json '(:|error| \"Product not found\"))))))\n#+end_src\n\nRegister the HTTP handler and start the app.\n\n#+begin_src lisp\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/product/:name\") #'product-handler)\n  CL-USER\u003e (jingle:start *app*)\n#+end_src\n\nTesting it out with different product names using =curl(1)=.\n\n#+begin_src shell\n  $ curl -s --get http://localhost:5000/api/v1/product/foo | jq '.'\n  {\n    \"id\": 1,\n    \"name\": \"foo\"\n  }\n\n  $ curl -s --get http://localhost:5000/api/v1/product/bar | jq '.'\n  {\n    \"id\": 2,\n    \"name\": \"bar\"\n  }\n\n  $ curl -s --get http://localhost:5000/api/v1/product/unknown | jq '.'\n  {\n    \"error\": \"Product not found\"\n  }\n#+end_src\n\nAnother example HTTP handler which returns a list of products in a\npaginated way, exposed via the =/api/v1/products= endpoint.\n\n#+begin_src lisp\n  (defun take (items from to)\n    \"A helper function to return the ITEMS between FROM and TO range\"\n    (let* ((len (length items))\n           (to (if (\u003e= to len) len to)))\n      (if (\u003e= from len)\n          nil\n          (subseq items from to))))\n\n  (defun products-handler (params)\n    \"Handles requests for /api/v1/product and returns a page of products\"\n    (jingle:set-response-status :ok)\n    (jingle:set-response-header :content-type \"application/json\")\n    ;; Parse the `FROM' and `TO' query parameters. Use default values of\n    ;; 0 and 5 for the params.\n    (let ((from (parse-integer (jingle:get-request-param params \"from\" \"0\") :junk-allowed t))\n          (to (parse-integer (jingle:get-request-param params \"to\" \"5\") :junk-allowed t)))\n      (cond\n        ((or (null from) (null to)) (jingle:set-response-status :bad-request) nil) ;; NIL added here for the response body\n        ((or (minusp from) (minusp to)) (jingle:set-response-status :bad-request) nil) ;; NIL added here for the response body\n        (t (jonathan:to-json (take *products* from to))))))\n#+end_src\n\nRegister the new API endpoint.\n\n#+begin_src lisp\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/products\") #'products-handler)\n#+end_src\n\nTesting it out using =curl(1)= with different values for =from= and\n=to= query params.\n\n#+begin_src shell\n  $ curl -s --get 'http://localhost:5000/api/v1/products?from=0\u0026to=2' | jq '.'\n  [\n    {\n      \"id\": 1,\n      \"name\": \"foo\"\n    },\n    {\n      \"id\": 2,\n      \"name\": \"bar\"\n    }\n  ]\n\n  $ curl -s --get 'http://localhost:5000/api/v1/products?from=2\u0026to=4' | jq '.'\n  [\n    {\n      \"id\": 3,\n      \"name\": \"baz\"\n    },\n    {\n      \"id\": 4,\n      \"name\": \"qux\"\n    }\n  ]\n#+end_src\n\nAnother way to retrieve request parameter values is to use the\n=JINGLE:WITH-REQUEST-PARAMS= macro. The previous example handler can\nbe rewritten this way.\n\n#+begin_src lisp\n  (defun products-handler (params)\n    (jingle:with-json-response\n      (jingle:with-request-params ((from-param \"from\" \"0\") (to-param \"to\" \"5\")) params\n        ;; Parse the query parameters and make sure we've got good values\n        (let ((from (parse-integer from-param :junk-allowed t))\n              (to (parse-integer to-param :junk-allowed t)))\n          (cond\n            ((or (null from) (null to))\n             (jingle:set-response-status :bad-request)\n             nil) ;; NIL added here for the response body\n            ((or (minusp from) (minusp to))\n             (jingle:set-response-status :bad-request)\n             nil) ;; NIL added here for the response body\n            (t (take *products* from to)))))))\n#+end_src\n\n** Macros\n\nThe following helper macros are available in =jingle=.\n\n- =JINGLE:WITH-JSON-RESPONSE=\n- =JINGLE:WITH-REQUEST-PARAMS=\n- =JINGLE:WITH-HTML-RESPONSE=\n\nThe =JINGLE:WITH-JSON-RESPONSE= macro sets up various HTTP headers\nsuch as =Content-Type= to =application/json= for you and evaluates the\nbody. The last evaluated expression from the body is encoded as a JSON\nobject using =JONATHAN:TO-JSON=.\n\nThe following example uses =LOCAL-TIME= and =JONATHAN= systems, so\nmake sure you have them loaded already.\n\n#+begin_src lisp\n  (defclass ping-response ()\n    ((message\n      :initarg :message\n      :initform \"pong\"\n      :reader ping-response-message\n      :documentation \"Message to send as part of the response\")\n     (timestamp\n      :initarg :timestamp\n      :initform (local-time:now)\n      :reader ping-response-timestamp))\n    (:documentation \"A response sent as part of a PING request\"))\n\n  (defmethod jonathan:%to-json ((object ping-response))\n    (jonathan:with-object\n      (jonathan:write-key-value \"message\" (ping-response-message object))\n      (jonathan:write-key-value \"timestamp\" (ping-response-timestamp object))))\n\n  (defun ping-handler (params)\n    (declare (ignore params))\n    (jingle:with-json-response\n      (make-instance 'ping-response)))\n#+end_src\n\nRegister the HTTP handler and start the app.\n\n#+begin_src lisp\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/ping\") #'ping-handler)\n  CL-USER\u003e (jingle:start *app*)\n#+end_src\n\nTrying it you should see results similar to the ones below.\n\n#+begin_src shell\n  $ curl -s --get http://localhost:5000/api/v1/ping | jq '.'\n  {\n    \"message\": \"pong\",\n    \"timestamp\": 1670593969\n  }\n\n  $ curl -s --get http://localhost:5000/api/v1/ping | jq '.'\n  {\n    \"message\": \"pong\",\n    \"timestamp\": 1670593974\n  }\n\n  $ curl -s --get http://localhost:5000/api/v1/ping | jq '.'\n  {\n    \"message\": \"pong\",\n    \"timestamp\": 1670593976\n  }\n#+end_src\n\nThe =JINGLE:WITH-REQUEST-PARAMS= macro provides an easy way to bind\nsymbols to request params from within HTTP handlers.\n\n#+begin_src lisp\n  (defun foo-handler (params)\n    (jingle:with-request-params ((foo \"foo\") (bar \"bar\")) params\n      ;; Use FOO and BAR params in order to ...\n      ...))\n#+end_src\n\nThe =JINGLE:WITH-HTML-RESPONSE= is similar to\n=JINGLE:WITH-JSON-RESPONSE=, but sets up the response with a\n=Content-Type: text/html; charset=utf-8= header.\n\n** Error Handling\n\nThe =JINGLE:BASE-HTTP-ERROR= condition may be used as the base for\nuser-defined conditions.\n\nIf a condition is signalled from within HTTP handlers and the\ncondition is a sub-class of =JINGLE:BASE-HTTP-ERROR=, then the\n=JINGLE:HANDLE-ERROR= method will be invoked.\n\nThe purpose of =JINGLE:HANDLE-ERROR= is to handle the error and set up\nan appropriate HTTP response, which will be returned to the client.\n\nThe rest of this section describes how to create and use custom errors\nfor a very simple REST API. The API we will develop provides the\nfollowing endpoints.\n\n#+begin_src shell\n  GET /api/v1/product        =\u003e Returns a list of products (supports `from` and `to` query params)\n  GET /api/v1/product/:name  =\u003e Returns a product by name, if found\n#+end_src\n\nThe error responses which we will return to clients would look like this.\n\n#+begin_src javascript\n  {\n      \"error\": \"\u003cReason for the error response\u003e\"\n  }\n#+end_src\n\nFirst we will define our =API-ERROR= condition, and then define the\n=JINGLE:HANDLE-ERROR= method on it, so that we return consistent error\nresponses to our API clients.\n\n#+begin_src lisp\n  (define-condition api-error (jingle:base-http-error)\n    ()\n    (:documentation \"Represents a condition which will be signalled on API errors\"))\n\n  (defmethod jingle:handle-error ((error api-error))\n    \"Handles the error and sets up the HTTP error response to be sent to clients\"\n    (with-accessors ((code jingle:http-error-code)\n                     (body jingle:http-error-body)) error\n      (jingle:set-response-status code)\n      (jingle:set-response-header :content-type \"application/json\")\n      (jonathan:to-json (list :|error| body))))\n#+end_src\n\nNext, we will implement some helper functions that signal common\nclient-error HTTP responses.\n\n#+begin_src lisp\n  (defun throw-not-found-error (message)\n    \"Throws a 404 (Not Found) HTTP response\"\n    (error 'api-error :code :not-found :body message))\n\n  (defun throw-bad-request-error (message)\n    \"Throws a 400 (Bad Request) HTTP response\"\n    (error 'api-error :code :bad-request :body message))\n#+end_src\n\nHaving our conditions and error-related functions we will also define\nanother helper function, which will be responsible for parsing HTTP\nquery parameters as integers, which we will use in our handlers.\n\n#+begin_src lisp\n  (defun get-int-param (params name \u0026optional default)\n    \"Gets the NAME parameter from PARAMS and parses it as an integer.\n  In case of invalid input it will signal a 400 (Bad Request) error\"\n    (let ((raw (jingle:get-request-param params name default)))\n      (typecase raw\n        (number raw)\n        (null (throw-bad-request-error (format nil \"missing value for `~A` param\" name)))\n        (string (let ((parsed (parse-integer raw :junk-allowed t)))\n                  (unless parsed\n                    (throw-bad-request-error (format nil \"invalid value for `~A` param\" name)))\n                  parsed))\n        (t (throw-bad-request-error (format nil \"unsupported value for `~A` param\" name))))))\n#+end_src\n\nWe will be building on top of the /products/ API, which was shown in a\nprevious section. The =*PRODUCTS*= var will be our \"database\" in this\nsimple API.\n\n#+begin_src lisp\n  (defparameter *products*\n    '((:|id| 1 :|name| \"foo\")\n      (:|id| 2 :|name| \"bar\")\n      (:|id| 3 :|name| \"baz\")\n      (:|id| 4 :|name| \"qux\")\n      (:|id| 5 :|name| \"foo v2\")\n      (:|id| 6 :|name| \"bar v3\")\n      (:|id| 7 :|name| \"baz v4\")\n      (:|id| 8 :|name| \"qux v5\"))\n    \"The list of our supported products\")\n\n  (defun find-product-by-name (name)\n    \"Finds a product by name\"\n    (find name\n          *products*\n          :key (lambda (item) (getf item :|name|))\n          :test #'string=))\n\n  (defun take (items from to)\n    \"A helper function to return the ITEMS between FROM and TO range\"\n    (let* ((len (length items))\n           (to (if (\u003e= to len) len to)))\n      (if (\u003e= from len)\n          nil\n          (subseq items from to))))\n#+end_src\n\nAnd these are the actual HTTP handlers, which will accept and handle\nclient requests.\n\n#+begin_src lisp\n  (defun get-product-handler (params)\n    \"Handles requests for the /api/v1/product/:name endpoint\"\n    (jingle:with-json-response\n      (let* ((name (jingle:get-request-param params :name))\n             (product (find-product-by-name name)))\n        (unless product\n          (throw-not-found-error \"product not found\"))\n        product)))\n\n  (defun get-products-page-handler (params)\n    \"Handles requests for the /api/v1/product endpoint\"\n    (jingle:with-json-response\n      (let ((from (get-int-param params \"from\" 0))\n            (to (get-int-param params \"to\" 2)))\n        (when (or (minusp from) (minusp to))\n          (throw-bad-request-error \"`from` and `to` must be positive\"))\n        (take *products* from to))))\n#+end_src\n\nFinally, we will create our =JINGLE:APP=, register our handlers and\nstart serving HTTP requests.\n\n#+begin_src lisp\n  CL-USER\u003e (defparameter *app* (jingle:make-app))\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/product\") #'get-products-page-handler)\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/product/:name\") #'get-product-handler)\n  CL-USER\u003e (jingle:start *app*)\n#+end_src\n\nTime to test things out.\n\n#+begin_src shell\n  # Getting a page of products using default `from` and `to` params\n  $ curl -s --get 'http://localhost:5000/api/v1/product' | jq '.'\n  [\n    {\n      \"id\": 1,\n      \"name\": \"foo\"\n    },\n    {\n      \"id\": 2,\n      \"name\": \"bar\"\n    }\n  ]\n\n  # Getting next page of products\n  $ curl -s --get 'http://localhost:5000/api/v1/product?from=2\u0026to=4' | jq '.'\n  [\n    {\n      \"id\": 3,\n      \"name\": \"baz\"\n    },\n    {\n      \"id\": 4,\n      \"name\": \"qux\"\n    }\n  ]\n\n  # Passing invalid query params\n  $ curl -s --get 'http://localhost:5000/api/v1/product?from=bad-value' | jq '.'\n  {\n    \"error\": \"invalid value for `from` param\"\n  }\n\n  # Passing negative values\n  $ curl -s --get 'http://localhost:5000/api/v1/product?to=-42' | jq '.'\n  {\n    \"error\": \"`from` and `to` must be positive\"\n  }\n\n  # Getting a product by name\n  $ curl -s --get 'http://localhost:5000/api/v1/product/foo' | jq '.'\n  {\n    \"id\": 1,\n    \"name\": \"foo\"\n  }\n\n  # Getting a non-existing product\n  $ curl -s --get 'http://localhost:5000/api/v1/product/unknown' | jq '.'\n  {\n    \"error\": \"product not found\"\n  }\n#+end_src\n\nGreat, things work as expected and our API clients will receive\nconsistent error responses with the proper HTTP status codes set.\n\n** Reverse URLs\n\nWhen you register routes in your app with names, you can then refer to\nthese routes by their names. This is useful in situations where you\nneed to get the URL for a particular route.\n\nIn order to get the URL for a route with a particular name use the\n=JINGLE:URL-FOR= generic function.\n\nConsider the HTTP handlers we have shown in the previous section of\nthis document.\n\n#+begin_src lisp\n  (defun get-product-handler (params)\n    \"Handles requests for the /api/v1/product/:name endpoint\"\n    (jingle:with-json-response\n      (let* ((name (jingle:get-request-param params :name))\n             (product (find-product-by-name name)))\n        (unless product\n          (throw-not-found-error \"product not found\"))\n        product)))\n\n  (defun get-products-page-handler (params)\n    \"Handles requests for the /api/v1/product endpoint\"\n    (jingle:with-json-response\n      (let ((from (get-int-param params \"from\" 0))\n            (to (get-int-param params \"to\" 2)))\n        (when (or (minusp from) (minusp to))\n          (throw-bad-request-error \"`from` and `to` must be positive\"))\n        (take *products* from to))))\n#+end_src\n\nWe can register these handlers and associate them with a name, which we\ncan later refer to.\n\n#+begin_src lisp\n  CL-USER\u003e (defparameter *app* (jingle:make-app))\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/product\" :method :get :identifier \"get-products-page\")\n                 #'get-products-page-handler)\n  CL-USER\u003e (setf (jingle:route *app* \"/api/v1/product/:name\" :method :get :identifier \"get-product-by-name\")\n                 #'get-product-handler)\n#+end_src\n\nNow, we can get the actual URLs for our HTTP handlers by using their\nnames.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:url-for *app* \"get-product-by-name\" :name \"foo\")\n  #\u003cQURI.URI:URI /api/v1/product/foo\u003e\n  CL-USER\u003e (jingle:url-for *app* \"get-products-page\" :|from| 0 :|to| 100)\n  #\u003cQURI.URI:URI /api/v1/product?from=0\u0026to=100\u003e\n#+end_src\n\nResolving URLs using =JINGLE:URL-FOR= is also useful when you are\ncreating test cases for your HTTP handlers. Within your test cases\ninstead of manually constructing the URLs to the respective HTTP\nhandlers you may refer to them by using their names.\n\nMake sure to also check the =JINGLE.DEMO= system, which uses a\n/handler registry/, which is used for registering the HTTP handlers\nfor a =JINGLE:APP=.\n\nOnce you resolve the URL for a particular handler you can construct\nthe final URL, which will contain scheme, hostname, etc.\n\n#+begin_src lisp\n  CL-USER\u003e (jingle:url-for *app* \"get-product-by-name\" :name \"foo\")\n  #\u003cQURI.URI:URI /api/v1/product/foo\u003e\n  CL-USER\u003e (quri:merge-uris * (quri:make-uri :host \"example.org\" :scheme \"https\"))\n  #\u003cQURI.URI.HTTP:URI-HTTPS https://example.org/api/v1/product/foo\u003e\n#+end_src\n\n** Testing HTTP Handlers\n\nThe =JINGLE:TEST-APP= is a test app meant to be used for test\ncases. The difference between =JINGLE:TEST-APP= and =JINGLE:APP= is\nthat the test app always binds on =127.0.0.1= and listens on a random\nport within a given range.\n\nAlso, when using =JINGLE:URL-FOR= generic function with a\n=JINGLE:TEST-APP= the result is a full URL, which contains the scheme,\nthe hostname and the port of the running test HTTP server.\n\nMake sure to check the =JINGLE.DEMO.TEST= system for some examples,\nwhich provides the test suite for the demo application.\n\n* Contributing\n\n=jingle= is hosted on [[https://github.com/dnaeon/cl-jingle][Github]]. Please contribute by reporting issues,\nsuggesting features or by sending patches using pull requests.\n\n* License\n\nThis project is Open Source and licensed under the [[http://opensource.org/licenses/BSD-2-Clause][BSD License]].\n\n* Authors\n\n- Marin Atanasov Nikolov \u003cdnaeon@gmail.com\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdnaeon%2Fcl-jingle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdnaeon%2Fcl-jingle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdnaeon%2Fcl-jingle/lists"}