Monolune

Simple web applications in Racket

Racket comes with built-in modules for web development. This guide aims to show the basics of using Racket to create and serve basic dynamic web pages. The example web application presented will be simple, but this should be a good start towards real world web development. This guide assumes minimal knowledge of Racket. Let's start.

A web server is used to serve web pages. A web server takes a request and returns a response. A request can be thought of as a user's request for a web page, and a response can be thought of as the content that is sent to the user's web browser in response to the request. Racket comes with a built-in web server.

Requests and Responses

Here is a program that responds to requests:

#lang racket
(require web-server/servlet)
(require web-server/servlet-env)

;; Returns a HTTP response given a HTTP request.
(define (request-handler request)
  (response/full
    200                  ; HTTP response code.
    #"OK"                ; HTTP response message.
    (current-seconds)    ; Timestamp.
    TEXT/HTML-MIME-TYPE  ; MIME type for content.
    '()                  ; Additional HTTP headers.
    (list                ; Content (in bytes) to send to the browser.
      #"<h1>Hello!</h1>"
      #"<p>It's a great day!</p>")))

;; Start the server.
(serve/servlet
  request-handler
  #:launch-browser? #f
  #:quit? #f
  #:listen-ip "127.0.0.1"
  #:port 8000
  #:servlet-regexp #rx"my-server")

Explanation:

  • serve/servlet receives requests and passes the request to a request handler (in our case, the request handler is request-handler).
  • The request handler (request-handler) returns a HTTP response. This is the content that is sent to a user's web browser. In our case, "Hello! It's a great day!" is sent as a response.
  • serve/servlet has several options (e.g. #:listen-ip) that customizes the way the Racket web server works. The options in our case specify that the server is to be accessible at http://127.0.0.1:8000 and that the "root URL" is my-server).
  • To see the results in a web browser, run the program in DrRacket (or use racket --require <filename> from the command line), and navigate to http://127.0.0.1:8000/my-server (or equivalently http://localhost:8000).
  • Notice that the same content is served when accessing http://localhost:8000/my-server/blabla. This because our server was set up to respond with the same HTTP response for every request.

Web pages

We will now see how to serve different responses based on the request. (e.g. http://example.com/local-news should have a different content from http://example.com/store).

#lang racket
(require web-server/servlet)
(require web-server/servlet-env)

;; Helper procedure to make returning HTTP responses easier.
(define (http-response content)  ; The 'content' parameter should be a string.
  (response/full
    200                  ; HTTP response code.
    #"OK"                ; HTTP response message.
    (current-seconds)    ; Timestamp.
    TEXT/HTML-MIME-TYPE  ; MIME type for content.
    '()                  ; Additional HTTP headers.
    (list                ; Content (in bytes) to send to the browser.
      (string->bytes/utf-8 content))))

(define (first-page request)
  (http-response "<h1>This is the first page!</h1>"))

(define (second-page request)
  (http-response "<h1>This is the second page!</h1>"))

;; URL routing table (URL dispatcher).
(define-values (dispatch generate-url)
               (dispatch-rules
                 [("first") first-page]
                 [("second") second-page]
                 [else (error "There is no procedure to handle the url.")]))

;; Notice how request-handler has changed from the previous example.
;; It now directs all requests to the URL dispatcher.
(define (request-handler request)
  (dispatch request))

;; Start the server.
(serve/servlet
  request-handler
  #:launch-browser? #f
  #:quit? #f
  #:listen-ip "127.0.0.1"
  #:port 8000
  #:servlet-regexp #rx"my-server")

Run the program and then in a web browser, navigate to http://localhost:8000/my-server/first, and after that, visit http://localhost:8000/my-server/second. Notice that the content served is different for each page.

Explanation:

  • First, notice that we have abstracted away the way we return HTTP responses into a procedure (http-response). It takes a string as its argument.
  • Second, notice the addition of a URL routing table. dispatch takes a request, and based on that request, it selects the appropriate procedure to handle that request. In our example, the request to http://localhost:8000/my-server/first will be routed to the procedure first-page which returns a HTTP response.

Overcoming some limitations:

  • If you want to serve pages starting at / instead of /my-server (e.g. http://localhost/second instead of http://localhost:8000/my-server/second), use #:servlet-regexp #rx"".
  • The pages we have served so far are essentially 'static' (i.e. the contents of the pages are unchanging). Pages can be made 'dynamic' by just varying the responses that the procedures return.

For example:

(define (show-time-page request)
  (http-response (number->string (current-seconds))))

Once an appropriate entry for the procedure has been added to the URL routing table, a different number will be returned in the response each time the web web page is accessed.

An improved version

#lang racket
(require web-server/servlet)  ; Provides dispatch-rules.
; Provides serve/servlet and happens to provide response/full.
(require web-server/servlet-env)

(define (http-response content)  ; The 'content' parameter should be a string.
  (response/full
    200                  ; HTTP response code.
    #"OK"                ; HTTP response message.
    (current-seconds)    ; Timestamp.
    TEXT/HTML-MIME-TYPE  ; MIME type for content.
    '()                  ; Additional HTTP headers.
    (list                ; Content (in bytes) to send to the browser.
      (string->bytes/utf-8 content))))

(define (first-page request)
  (http-response "<h1>This is the first page!</h1>"))

(define (second-page request)
  (http-response "<h1>This is the second page!</h1>"))

(define (show-time-page request)
  (http-response (number->string (current-seconds))))

(define (greeting-page request human-name)  ; Notice the additional parameter.
  (http-response (string-append "Hello " human-name "!")))

;; URL routing table (URL dispatcher).
(define-values (dispatch generate-url)
               (dispatch-rules
                 [("first-page") first-page]
                 [("second-page") second-page]
                 [("time") show-time-page]
                 [("hello" (string-arg)) greeting-page]  ; Notice this line.
                 [else (error "There is no procedure to handle the url.")]))

(define (request-handler request)
  (dispatch request))

;; Start the server.
(serve/servlet
  request-handler
  #:launch-browser? #f
  #:quit? #f
  #:listen-ip "127.0.0.1"
  #:port 8000
  #:servlet-regexp #rx"")

The program can be run from DrRacket, or racket --require <filename> from the command line.

In the example above, the pages can be accessed at http://localhost:8000/first, http://localhost:8000/second, and http://localhost:8000/time.

In addition, I've added a special page. Try to navigate to http://localhost:8000/hello/lewis and http://localhost:8000/hello/marie. Notice how the content of the page depends directly on the URL. The (string-arg) in the URL routing table essentially represents a string that will be passed to the procedure that handles the URL.

Conclusion

This concludes the guide to creating basic web applications in Racket. Admittedly, there is much more to web development than what is presented in this guide. For example, cookies, sessions, forms, database connections, and static files go beyond the scope of this article, but are necessary for production-grade web development. For those topics, the Racket documentation is helpful, as are the official Racket mailing lists and the questions on Stack Overflow.

Here are the Racket documentation references for the procedures used in this guide: