lib/popcorn: document use_before and use_after
[nit.git] / lib / popcorn / README.md
1 # Popcorn
2
3 **Why endure plain corn when you can pop it?!**
4
5 Popcorn is a minimal yet powerful nit web application framework that provides cool
6 features for lazy developpers.
7
8 Popcorn is built over nitcorn to provide a clean and user friendly interface
9 without all the boiler plate code.
10
11 ## What does it taste like?
12
13 Set up is quick and easy as 10 lines of code.
14 Create a file `app.nit` and add the following code:
15
16 ~~~
17 import popcorn
18
19 class HelloHandler
20         super Handler
21
22         redef fun get(req, res) do res.html "<h1>Hello World!</h1>"
23 end
24
25 var app = new App
26 app.use("/", new HelloHandler)
27 app.listen("localhost", 3000)
28 ~~~
29
30 The Popcorn app listens on port 3000 for connections.
31 The app responds with "Hello World!" for requests to the root URL (`/`) or **route**.
32 For every other path, it will respond with a **404 Not Found**.
33
34 The `req` (request) and `res` (response) parameters are the same that nitcorn provides
35 so you can do anything else you would do in your route without Popcorn involved.
36
37 Run the app with the following command:
38
39 ~~~bash
40 $ nitc app.nit && ./app
41 ~~~
42
43 Then, load [http://localhost:3000](http://localhost:3000) in a browser to see the output.
44
45 Here the output using the `curl` command:
46
47 ~~~bash
48 $ curl localhost:3000
49 <h1>Hello World!</h1>
50
51 $ curl localhost:3000/wrong_uri
52 <!DOCTYPE html>
53 <html>
54 <head>
55 <meta charset="utf-8">
56 <title>Not Found</title>
57 </head>
58 <body>
59 <h1>404 Not Found</h1>
60 </body>
61 </html>
62 ~~~
63
64 This is why we love popcorn!
65
66 ## Basic routing
67
68 **Routing** refers to determining how an application responds to a client request
69 to a particular endpoint, which is a URI (or path) and a specific HTTP request
70 method GET, POST, PUT or DELETE (other methods are not suported yet).
71
72 Each route can have one or more handler methods, which are executed when the route is matched.
73
74 Route handlers definition takes the following form:
75
76 ~~~nitish
77 import popcorn
78
79 class MyHandler
80         super Handler
81
82         redef fun METHOD(req, res) do end
83 end
84 ~~~
85
86 Where:
87 * `MyHandler` is the name of the handler you will add to the app.
88 * `METHOD` can be replaced by `get`, `post`, `put` or `delete`.
89
90 The following example responds to GET and POST requests:
91
92 ~~~
93 import popcorn
94
95 class MyHandler
96         super Handler
97
98         redef fun get(req, res) do res.send "Got a GET request"
99         redef fun post(req, res) do res.send "Got a POST request"
100 end
101 ~~~
102
103 To make your handler responds to a specific route, you have to add it to the app.
104
105 Respond to POST request on the root route (`/`), the application's home page:
106
107 ~~~
108 var app = new App
109 app.use("/", new MyHandler)
110 ~~~
111
112 Respond to a request to the `/user` route:
113
114 ~~~
115 app.use("/user", new MyHandler)
116 ~~~
117
118 For more details about routing, see the routing section.
119
120 ## Serving static files with Popcorn
121
122 To serve static files such as images, CSS files, and JavaScript files, use the
123 Popcorn built-in handler `StaticHandler`.
124
125 Pass the name of the directory that contains the static assets to the StaticHandler
126 init method to start serving the files directly.
127 For example, use the following code to serve images, CSS files, and JavaScript files
128 in a directory named `public`:
129
130 ~~~
131 app.use("/", new StaticHandler("public/"))
132 ~~~
133
134 Now, you can load the files that are in the `public` directory:
135
136 ~~~raw
137 http://localhost:3000/images/trollface.jpg
138 http://localhost:3000/css/style.css
139 http://localhost:3000/js/app.js
140 http://localhost:3000/hello.html
141 ~~~
142
143 Popcorn looks up the files relative to the static directory, so the name of the
144 static directory is not part of the URL.
145 To use multiple static assets directories, add the `StaticHandler` multiple times:
146
147 ~~~
148 app.use("/", new StaticHandler("public/"))
149 app.use("/", new StaticHandler("files/"))
150 ~~~
151
152 Popcorn looks up the files in the order in which you set the static directories
153 with the `use` method.
154
155 To create a virtual path prefix (where the path does not actually exist in the file system)
156 for files that are served by the `StaticHandler`, specify a mount path for the
157 static directory, as shown below:
158
159 ~~~
160 app.use("/static/", new StaticHandler("public/"))
161 ~~~
162
163 Now, you can load the files that are in the public directory from the `/static`
164 path prefix.
165
166 ~~~raw
167 http://localhost:3000/static/images/trollface.jpg
168 http://localhost:3000/static/css/style.css
169 http://localhost:3000/static/js/app.js
170 http://localhost:3000/static/hello.html
171 ~~~
172
173 However, the path that you provide to the `StaticHandler` is relative to the
174 directory from where you launch your app.
175 If you run the app from another directory, it’s safer to use the absolute path of
176 the directory that you want to serve.
177
178 ## Advanced Routing
179
180 **Routing** refers to the definition of application end points (URIs) and how
181 they respond to client requests. For an introduction to routing, see the Basic routing
182 section.
183
184 The following code is an example of a very basic route.
185
186 ~~~
187 import popcorn
188
189 class HelloHandler
190         super Handler
191
192         redef fun get(req, res) do res.send "Hello World!"
193 end
194
195 var app = new App
196 app.use("/", new HelloHandler)
197 ~~~
198
199 ### Route methods
200
201 A **route method** is derived from one of the HTTP methods, and is attached to an
202 instance of the Handler class.
203
204 The following code is an example of routes that are defined for the GET and the POST
205 methods to the root of the app.
206
207 ~~~
208 import popcorn
209
210 class GetPostHandler
211         super Handler
212
213         redef fun get(req, res) do res.send "GET request to the homepage"
214         redef fun post(req, res) do res.send "POST request to the homepage"
215 end
216
217 var app = new App
218 app.use("/", new GetPostHandler)
219 ~~~
220
221 Popcorn supports the following routing methods that correspond to HTTP methods:
222 get, post, put, and delete.
223
224 The request query string is accessed through the `req` parameter:
225
226 ~~~
227 import popcorn
228 import template
229
230 class QueryStringHandler
231         super Handler
232
233         redef fun get(req, res) do
234                 var tpl = new Template
235                 tpl.addn "URI: {req.uri}"
236                 tpl.addn "Query string: {req.query_string}"
237                 for name, arg in req.get_args do
238                         tpl.addn "{name}: {arg}"
239                 end
240         res.send tpl
241         end
242 end
243
244 var app = new App
245 app.use("/", new QueryStringHandler)
246 app.listen("localhost", 3000)
247 ~~~
248
249 Post parameters can also be accessed through the `req` parameter:
250
251 ~~~
252 import popcorn
253 import template
254
255 class PostHandler
256         super Handler
257
258         redef fun post(req, res) do
259                 var tpl = new Template
260                 tpl.addn "URI: {req.uri}"
261                 tpl.addn "Body: {req.body}"
262                 for name, arg in req.post_args do
263                         tpl.addn "{name}: {arg}"
264                 end
265         res.send tpl
266         end
267 end
268
269 var app = new App
270 app.use("/", new PostHandler)
271 app.listen("localhost", 3000)
272 ~~~
273
274 There is a special routing method, `all(res, req)`, which is not derived from any
275 HTTP method. This method is used to respond at a path for all request methods.
276
277 In the following example, the handler will be executed for requests to "/user"
278 whether you are using GET, POST, PUT, DELETE, or any other HTTP request method.
279
280 ~~~
281 import popcorn
282
283 class AllHandler
284         super Handler
285
286         redef fun all(req, res) do res.send "Every request to the homepage"
287 end
288 ~~~
289
290 Using the `all` method you can also implement other HTTP request methods.
291
292 ~~~
293 import popcorn
294
295 class MergeHandler
296         super Handler
297
298         redef fun all(req, res) do
299                 if req.method == "MERGE" then
300                         # handle that method
301                 else super # keep handle GET, POST, PUT and DELETE methods
302         end
303 end
304 ~~~
305
306 ### Route paths
307
308 **Route paths**, in combination with a request handlers, define the endpoints at
309 which requests can be made.
310 Route paths can be strings, parameterized strings or glob patterns.
311 Query strings such as `?q=foo`are not part of the route path.
312
313 Popcorn uses the `Handler::match(uri)` method to match the route paths.
314
315 Here are some examples of route paths based on strings.
316
317 This route path will match requests to the root route, `/`.
318
319 ~~~
320 import popcorn
321
322 class MyHandler
323         super Handler
324
325         redef fun get(req, res) do res.send "Got a GET request"
326 end
327
328 var app = new App
329 app.use("/", new MyHandler)
330 ~~~
331
332 This route path will match requests to `/about`.
333
334 ~~~
335 app.use("/about", new MyHandler)
336 ~~~
337
338 This route path will match requests to `/random.text`.
339
340 ~~~
341 app.use("/random.text", new MyHandler)
342 ~~~
343
344 During the query/response process, routes are matched by order of declaration
345 through the `App::use` method.
346
347 The app declared in this example will try to match the routes in this order:
348
349 1. `/`
350 2. `/about`
351 3. `/random.text`
352
353 ### Route parameters
354
355 **Route parameters** are variable parts of a route path. They can be used to path
356 arguments within the URI.\13
357 Parameters in a route are prefixed with a colon `:` like in `:userId`, `:year`.
358
359 The following example declares a handler `UserHome` that responds with the `user`
360 name.
361
362 ~~~
363 import popcorn
364
365 class UserHome
366         super Handler
367
368         redef fun get(req, res) do
369                 var user = req.param("user")
370                 if user != null then
371                         res.send "Hello {user}"
372                 else
373                         res.send("Nothing received", 400)
374                 end
375         end
376 end
377
378 var app = new App
379 app.use("/:user", new UserHome)
380 app.listen("localhost", 3000)
381 ~~~
382
383 The `UserHome` handler listen to every path matching `/:user`. This can be `/Morriar`,
384 `/10`, ... but not `/Morriar/profile` since route follow the strict matching rule.
385
386 ### Glob routes
387
388 **Glob routes** are routes that match only on a prefix, thus accepting a wider range
389 of URI.
390 Glob routes end with the symbol `*`.
391
392 Here we define a `UserItem` handler that will respond to any URI matching the prefix
393 `/user/:user/item/:item`.
394 Note that glob route are compatible with route parameters.
395
396 ~~~
397 import popcorn
398
399 class UserItem
400         super Handler
401
402         redef fun get(req, res) do
403                 var user = req.param("user")
404                 var item = req.param("item")
405                 if user == null or item == null then
406                         res.send("Nothing received", 400)
407                 else
408                         res.send "Here the item {item} of the use {user}."
409                 end
410         end
411 end
412
413 var app = new App
414 app.use("/user/:user/item/:item/*", new UserItem)
415 app.listen("localhost", 3000)
416 ~~~
417
418 ## Response methods
419
420 The methods on the response object (`res`), can is used to manipulate the
421 request-response cycle.
422 If none of these methods are called from a route handler, the client request will
423 receive a `404 Not found` error.
424
425 * `res.html()` Send a HTML response.
426 * `res.json()` Send a JSON response.
427 * `res.redirect()` Redirect a request.
428 * `res.send()` Send a response of various types.
429 * `res.error()` Set the response status code and send its message as the response body.
430
431 ## Response cycle
432
433 When the popcorn `App` receives a request, the response cycle is the following:
434
435 1. `pre-middlewares` lookup matching middlewares registered with `use_before(pre_middleware)`:
436         1. execute matching middleware by registration order
437         2. if a middleware send a response then let the `pre-middlewares` loop continue
438            with the next middleware
439 2. `response-handlers` lookup matching handlers registered with `use(handler)`:
440         1. execute matching middleware by registration order
441         2. if a middleware send a response then stop the `response-handlers` loop
442         3. if no hander matches or sends a response, generate a 404 response
443 3. `post-middlewares` lookup matching handlers registered with `use_after(post_handler)`:
444         1. execute matching middleware by registration order
445         2. if a middleware send a response then let the `post-middlewares` loop continue
446            with the next middleware
447
448 ## Middlewares
449
450 ### Overview
451
452 **Middleware** handlers are handlers that typically do not send `HttpResponse` responses.
453 Middleware handlers can perform the following tasks:
454
455 * Execute any code.
456 * Make changes to the request and the response objects.
457 * End its action and pass to the next handler in the stack.
458
459 If a middleware handler makes a call to `res.send()`, it provoques the end the
460 request-response cycle and the response is sent to the client.
461
462 ### Ultra simple logger example
463
464 Here is an example of a simple “Hello World” Popcorn application.
465 We add a middleware handler to the application called MyLogger that prints a simple
466 log message in the app stdout.
467
468 ~~~
469 import popcorn
470
471 class MyLogger
472         super Handler
473
474         redef fun all(req, res) do print "Request Logged!"
475 end
476
477 class HelloHandler
478         super Handler
479
480         redef fun get(req, res) do res.send "Hello World!"
481 end
482
483
484 var app = new App
485 app.use_before("/*", new MyLogger)
486 app.use("/", new HelloHandler)
487 app.listen("localhost", 3000)
488 ~~~
489
490 By using the `MyLogger` handler to the route `/*` we ensure that every requests
491 (even 404 ones) pass through the middleware handler.
492 This handler just prints “Request Logged!” when a request is received.
493
494 Be default, the order of middleware execution is that are loaded first are also executed first.
495 To ensure our middleware `MyLogger` will be executed before all the other, we add it
496 with the `use_before` method.
497
498 ### Ultra cool, more advanced logger example
499
500 Next, we’ll create a middleware handler called “LogHandler” that prints the requested
501 uri, the response status and the time it took to Popcorn to process the request.
502
503 This example gives a simplified version of the `RequestClock` and `ConsoleLog` middlewares.
504
505 ~~~
506 import popcorn
507 import realtime
508
509 redef class HttpRequest
510         # Time that request was received by the Popcorn app.
511         var timer: nullable Clock = null
512 end
513
514 class RequestTimeHandler
515         super Handler
516
517         redef fun all(req, res) do req.timer = new Clock
518 end
519
520 class LogHandler
521         super Handler
522
523         redef fun all(req, res) do
524                 var timer = req.timer
525                 if timer != null then
526                         print "{req.method} {req.uri} {res.color_status} ({timer.total}s)"
527                 else
528                         print "{req.method} {req.uri} {res.color_status}"
529                 end
530         end
531 end
532
533 class HelloHandler
534         super Handler
535
536         redef fun get(req, res) do res.send "Hello World!"
537 end
538
539 var app = new App
540 app.use_before("/*", new RequestTimeHandler)
541 app.use("/", new HelloHandler)
542 app.use_after("/*", new LogHandler)
543 app.listen("localhost", 3000)
544 ~~~
545
546 First, we attach a new attribute `timer` to every `HttpRequest`.
547 Doing so we can access our data from all handlers that import our module, directly
548 from the `req` parameter.
549
550 We use the new middleware called `RequestTimeHandler` to initialize the request timer.
551 Because of the `use_before` method, the `RequestTimeHandler` middleware will be executed
552 before all the others.
553
554 We then let the `HelloHandler` produce the response.
555
556 Finally, our `LogHandler` will display a bunch of data and use the request `timer`
557 to display the time it took to process the request.
558 Because of the `use_after` method, the `LogHandler` middleware will be executed after
559 all the others.
560
561 The app now uses the `RequestTimeHandler` middleware for every requests received
562 by the Popcorn app.
563 The page is processed the `HelloHandler` to display the index page.
564 And, before every response is sent, the `LogHandler` is activated to display the
565 log line.
566
567 Because you have access to the request object, the response object, and all the
568 Popcorn API, the possibilities with middleware functions are endless.
569
570 ### Built-in middlewares
571
572 Starting with version 0.1, Popcorn provide a set of built-in middleware that can
573 be used to develop your app faster.
574
575 * `RequestClock`: initializes requests clock.
576 * `ConsoleLog`: displays resquest and response status in console (can be used with `RequestClock`).
577 * `SessionInit`: initializes requests session (see the `Sessions` section).
578 * `StaticServer`: serves static files (see the `Serving static files with Popcorn` section).
579 * `Router`: a mountable mini-app (see the `Mountable routers` section).
580
581 ## Mountable routers
582
583 Use the `Router` class to create modular, mountable route handlers.
584 A Router instance is a complete middleware and routing system; for this reason,
585 it is often referred to as a “mini-app”.
586
587 The following example creates a router as a module, loads a middleware handler in it,
588 defines some routes, and mounts the router module on a path in the main app.
589
590 ~~~
591 import popcorn
592
593 class AppHome
594         super Handler
595
596         redef fun get(req, res) do res.send "Site Home"
597 end
598
599 class UserLogger
600         super Handler
601
602         redef fun all(req, res) do print "User logged"
603 end
604
605 class UserHome
606         super Handler
607
608         redef fun get(req, res) do res.send "User Home"
609 end
610
611 class UserProfile
612         super Handler
613
614         redef fun get(req, res) do res.send "User Profile"
615 end
616
617 var user_router = new Router
618 user_router.use("/*", new UserLogger)
619 user_router.use("/", new UserHome)
620 user_router.use("/profile", new UserProfile)
621
622 var app = new App
623 app.use("/", new AppHome)
624 app.use("/user", user_router)
625 app.listen("localhost", 3000)
626 ~~~
627
628 The app will now be able to handle requests to /user and /user/profile, as well
629 as call the `Time` middleware handler that is specific to the route.
630
631 ## Error handling
632
633 **Error handling** is based on middleware handlers.
634
635 Define error-handling middlewares in the same way as other middleware handlers:
636
637 ~~~
638 import popcorn
639
640 class SimpleErrorHandler
641         super Handler
642
643         redef fun all(req, res) do
644                 if res.status_code != 200 then
645                         print "An error occurred! {res.status_code})"
646                 end
647         end
648 end
649
650 class HelloHandler
651         super Handler
652
653         redef fun get(req, res) do res.send "Hello World!"
654 end
655
656 var app = new App
657 app.use("/", new HelloHandler)
658 app.use("/*", new SimpleErrorHandler)
659 app.listen("localhost", 3000)
660 ~~~
661
662 In this example, every non-200 response is caught by the `SimpleErrorHandler`
663 that print an error in stdout.
664
665 By defining multiple middleware error handlers, you can take multiple action depending
666 on the kind of error or the kind of interface you provide (HTML, XML, JSON...).
667
668 Here an example of the 404 custom error page in HTML:
669
670 ~~~
671 import popcorn
672 import template
673
674 class HtmlErrorTemplate
675         super Template
676
677         var status: Int
678         var message: nullable String
679
680         redef fun rendering do add """
681                 <!DOCTYPE html>
682                 <html>
683                 <head>
684                         <meta charset="utf-8">
685                         <title>{{{message or else status}}}</title>
686                 </head>
687                 <body>
688                 <h1>{{{status}}} {{{message or else ""}}}</h1>
689                 </body>
690                 </html>"""
691 end
692
693 class HtmlErrorHandler
694         super Handler
695
696         redef fun all(req, res) do
697                 if res.status_code != 200 then
698                         res.send(new HtmlErrorTemplate(res.status_code, "An error occurred!"))
699                 end
700         end
701 end
702
703 var app = new App
704 app.use("/*", new HtmlErrorHandler)
705 app.listen("localhost", 3000)
706 ~~~
707
708 ## Sessions
709
710 **Sessions** can be used thanks to the built-in `SessionMiddleware`.
711
712 Here a simple example of login button that define a value in the `req` session.
713
714 ~~~
715 import popcorn
716
717 redef class Session
718         var is_logged = false
719 end
720
721 class AppLogin
722         super Handler
723
724         redef fun get(req, res) do
725                 res.html """
726                 <p>Is logged: {{{req.session.as(not null).is_logged}}}</p>
727                 <form action="/" method="POST">
728                         <input type="submit" value="Login" />
729                 </form>"""
730         end
731
732         redef fun post(req, res) do
733                 req.session.as(not null).is_logged = true
734                 res.redirect("/")
735         end
736 end
737
738 var app = new App
739 app.use("/*", new SessionInit)
740 app.use("/", new AppLogin)
741 app.listen("localhost", 3000)
742 ~~~
743
744 Notice the use of the `SessionInit` on the `/*` route. You must use the
745 `SessionInit` first to initialize the request session.
746 Without that, your request session will be set to `null`.
747 If you don't use sessions in your app, you do not need to include that middleware.
748
749 ## Database integration
750
751 ### Mongo DB
752
753 If you want to persist your data, Popcorn works well with MongoDB.
754
755 In this example, we will show how to store and list user with a Mongo database.
756
757 First let's define a handler that access the database to list all the user.
758 The mongo database reference is passed to the UserList handler through the `db` attribute.
759
760 Then we define a handler that displays the user creation form on GET requests.
761 POST requests are used to save the user data.
762
763 ~~~
764 import popcorn
765 import mongodb
766 import template
767
768 class UserList
769         super Handler
770
771         var db: MongoDb
772
773         redef fun get(req, res) do
774                 var users = db.collection("users").find_all(new JsonObject)
775
776                 var tpl = new Template
777                 tpl.add "<h1>Users</h1>"
778                 tpl.add "<table>"
779                 for user in users do
780                         tpl.add """<tr>
781                                 <td>{{{user["login"] or else "null"}}}</td>
782                                 <td>{{{user["password"] or else "null"}}}</td>
783                         </tr>"""
784                 end
785                 tpl.add "</table>"
786                 res.html tpl
787         end
788 end
789
790 class UserForm
791         super Handler
792
793         var db: MongoDb
794
795         redef fun get(req, res) do
796                 var tpl = new Template
797                 tpl.add """<h1>Add a new user</h1>
798                 <form action="/new" method="POST">
799                         <input type="text" name="login" />
800                         <input type="password" name="password" />
801                         <input type="submit" value="save" />
802                 </form>"""
803                 res.html tpl
804         end
805
806         redef fun post(req, res) do
807                 var json = new JsonObject
808                 json["login"] = req.post_args["login"]
809                 json["password"] = req.post_args["password"]
810                 db.collection("users").insert(json)
811                 res.redirect "/"
812         end
813 end
814
815 var mongo = new MongoClient("mongodb://localhost:27017/")
816 var db = mongo.database("mongo_example")
817
818 var app = new App
819 app.use("/", new UserList(db))
820 app.use("/new", new UserForm(db))
821 app.listen("localhost", 3000)
822 ~~~
823
824 ## Angular.JS integration
825
826 Loving [AngularJS](https://angularjs.org/)? Popcorn is made for Angular and for you!
827
828 Using the StaticHandler with a glob route, you can easily redirect all HTTP requests
829 to your angular controller:
830
831 ~~~
832 import popcorn
833
834 var app = new App
835 app.use("/*", new StaticHandler("my-ng-app/"))
836 app.listen("localhost", 3000)
837 ~~~
838
839 See the examples for a more detailed use case working with a JSON API.