Racket Web Applications Cheat Sheet
Setting Up a Racket Web Server
Basic Server Configuration
1(require (prefix-in servlet: web-server/servlet-env))
2
3(servlet:serve/servlet
4 dispatcher-function
5 #:listen-ip "0.0.0.0"
6 #:port 8080
7 #:launch-browser? #f
8 #:quit? #f
9 #:servlet-path "/"
10 #:servlet-regexp #rx""
11 #:stateless? #t)
Pattern: Use #:stateless? #t for REST APIs that don’t need session continuations.
Main Module Entry Point
1(module+ main
2 (servlet:serve/servlet
3 route-dispatch/with-middleware
4 #:listen-ip "0.0.0.0"
5 #:port 8080
6 #:launch-browser? #f
7 #:quit? #f
8 #:servlet-path "/"
9 #:servlet-regexp #rx""
10 #:stateless? #t))
URL Routing and Dispatching
Dispatch Rules
1(require (prefix-in dispatch: web-server/dispatch))
2
3(define-values (route-dispatch request-parser)
4 (dispatch:dispatch-rules
5 [("") homepage-handler]
6 [("api" "users") #:method "post" create-user-handler]
7 [("api" "users" (string-arg)) get-user-handler]
8 [("api" "data" (integer-arg)) #:method "delete" delete-data-handler]
9 [else not-found-handler]))
Gotcha: The dispatch-rules macro returns two values - the dispatcher and a request parser. Use define-values.
URL Parameters
1; String argument from URL path
2[("users" (string-arg)) user-handler]
3; matches /users/123, /users/john, etc.
4
5; Integer argument from URL path
6[("items" (integer-arg)) item-handler]
7; matches /items/42, fails on /items/abc
8
9; Handler receives the argument
10(define (user-handler request user-id)
11 (response/jsexpr (hash 'user-id user-id)))
HTTP Method Routing
1(dispatch:dispatch-rules
2 [("api" "resource") #:method "get" list-handler]
3 [("api" "resource") #:method "post" create-handler]
4 [("api" "resource" (string-arg)) #:method "put" update-handler]
5 [("api" "resource" (string-arg)) #:method "delete" delete-handler])
Handling HTTP Requests: Parameters, Headers, and Body
Request Structure
1(require web-server/http)
2
3; Request fields
4(request-method request) ; => #"GET", #"POST", etc.
5(request-uri request) ; => url struct
6(request-headers request) ; => list of headers
7(request-bindings request) ; => POST/GET parameters
8(request-post-data/raw request) ; => raw POST body bytes
9(request-host-ip request) ; => client IP
Extracting POST Parameters
1(require web-server/http/bindings)
2
3; Extract single parameter
4(define (post-param name request)
5 (extract-binding/single name (request-bindings request)))
6
7; Usage
8(define username (post-param 'username request))
9(define email (post-param 'email request))
Gotcha: extract-binding/single raises an exception if:
- The parameter doesn’t exist
- Multiple parameters with the same name exist
Converting All Bindings to Hash
1(define (bindings->hash request)
2 (for/hash ([b (request-bindings/raw request)])
3 (values (binding-id b)
4 (bytes->string/utf-8 (binding:form-value b)))))
5
6; Usage
7(define params (bindings->hash request))
8(hash-ref params #"username" #f) ; => "john" or #f
Handling Bytes vs Strings
1; Bindings may return bytes even though docs say string
2(define param-value (post-param 'field-name request))
3
4; Always handle both cases for robustness
5(define safe-value
6 (if (bytes? param-value)
7 (bytes->string/utf-8 param-value)
8 param-value))
Constructing HTTP Responses: JSON, XML, and Custom Headers
JSON Responses
1(require web-server/http/json)
2
3(define (api-handler request)
4 (response/jsexpr
5 #:code 200
6 (hash 'status "success"
7 'data (list 1 2 3)
8 'count 3)))
XML Responses (X-expressions)
1(require web-server/http/xexpr)
2
3(define (xml-handler request)
4 (response/xexpr
5 #:code 200
6 `(response
7 (status ((code "200")) "OK")
8 (data
9 (item ((id "1")) "First")
10 (item ((id "2")) "Second")))))
Custom Responses
1(define (custom-handler request)
2 (response/full
3 200 ; status code
4 #f ; message (inferred from status)
5 (current-seconds) ; timestamp
6 #"application/json" ; MIME type
7 '() ; additional headers list
8 (list #"{\"status\": \"ok\"}"))) ; body as list of byte strings
Gotcha: Response body must be a list of byte strings, not a single string.
1; WRONG
2(response/full 200 #f (current-seconds) #"text/plain" '() "body")
3
4; CORRECT
5(response/full 200 #f (current-seconds) #"text/plain" '()
6 (list (string->bytes/utf-8 "body")))
Response with Custom Headers
1(require web-server/http/response-structs)
2
3(define (handler-with-headers request)
4 (response/full
5 200
6 #f
7 (current-seconds)
8 #"text/plain"
9 (list (header #"X-Custom-Header" #"custom-value")
10 (header #"Cache-Control" #"no-cache"))
11 (list #"Response body")))
Common Response Patterns
1; 404 Not Found
2(define (not-found-handler request)
3 (response/jsexpr
4 #:code 404
5 (hash 'status "error"
6 'message "Not Found")))
7
8; 500 Server Error
9(define (error-handler request exn)
10 (response/jsexpr
11 #:code 500
12 (hash 'status "error"
13 'message (exn-message exn))))
14
15; 201 Created
16(define (create-handler request)
17 (define new-id (create-resource))
18 (response/jsexpr
19 #:code 201
20 (hash 'status "created"
21 'id new-id)))
Implementing Middleware for Logging and Error Handling
Error Handling Middleware
1(define (dispatch-with-error-handling request)
2 (with-handlers ([exn:fail? (lambda (exn) (error-handler request exn))])
3 (route-dispatch request)))
Pattern: Wrap the main dispatcher with with-handlers to catch all unhandled exceptions.
Full Error Handler with Stack Trace
1(define (error-handler request exn)
2 (define error-message (exn-message exn))
3 (define stack-trace
4 (if (exn:fail? exn)
5 (continuation-mark-set->context (exn-continuation-marks exn))
6 '()))
7
8 (response/xexpr
9 #:code 500
10 `(response
11 (error
12 (message "Internal Server Error")
13 (details ,error-message)
14 (stack-trace
15 ,@(map (lambda (frame) `(frame ,(format "~a" frame)))
16 stack-trace))))))
Request Logging Middleware
1(require (prefix-in logresp: web-server/dispatchers/dispatch-logresp))
2
3(define (dispatch-with-logging request)
4 (define response (route-dispatch request))
5 (display (logresp:apache-default-format request response))
6 response)
Pattern: Log after getting the response to include status code and timing information.
Combined Middleware
1(define (route-dispatch/with-middleware request)
2 (define response
3 (with-handlers ([exn:fail? (lambda (exn) (error-handler request exn))])
4 (route-dispatch request)))
5
6 ; Log request and response
7 (display (logresp:apache-default-format request response))
8 response)
Testing Web Applications: Unit and Integration Tests
Test Server Setup
1(module+ test
2 (require web-server/web-server
3 web-server/servlet-dispatch)
4
5 ; Define test handler
6 (define (test-handler req)
7 (response/full 200 #"OK" (current-seconds) #"text/plain"
8 '() (list #"test response")))
9
10 ; Start test server (non-blocking)
11 (define stop-server
12 (serve
13 #:dispatch (dispatch/servlet test-handler)
14 #:port 8089
15 #:listen-ip "127.0.0.1"))
16
17 ; Wait for server to start
18 (sleep 0.1)
19
20 ; Run tests...
21 (test-case "server responds"
22 (define response (get "http://localhost:8089/"))
23 (check-equal? (response-status-code response) 200))
24
25 ; Cleanup
26 (stop-server))
Pattern: Use serve (returns stop function) instead of serve/servlet (blocks) for tests.
Mock Request Creation
1(require web-server/http/request-structs
2 net/url)
3
4(define (make-mock-request #:method [method #"GET"]
5 #:url [url "http://localhost/"]
6 #:bindings [bindings '()])
7 (request method
8 (string->url url)
9 '() ; headers
10 (delay bindings) ; POST bindings (delayed)
11 #f ; post-data
12 "127.0.0.1" ; host-ip
13 80 ; host-port
14 "127.0.0.1")) ; client-ip
Pattern: Use delayed bindings (delay) to match the request struct’s expected type.
Mock Binding Creation
1(define (make-mock-binding name value)
2 (make-binding:form
3 (string->bytes/utf-8 (symbol->string name))
4 (string->bytes/utf-8 value)))
5
6; Usage in tests
7(define test-bindings
8 (list (make-mock-binding 'username "testuser")
9 (make-mock-binding 'email "[email protected]")))
10
11(define test-request
12 (make-mock-request
13 #:method #"POST"
14 #:bindings test-bindings))
Testing Handlers
1(module+ test
2 (require rackunit)
3
4 (test-case "handler returns correct status"
5 (define req (make-mock-request))
6 (define resp (homepage-handler req))
7 (check-equal? (response-code resp) 200))
8
9 (test-case "handler extracts POST params"
10 (define bindings (list (make-mock-binding 'username "john")))
11 (define req (make-mock-request #:method #"POST" #:bindings bindings))
12 (define resp (create-user-handler req))
13 (check-equal? (response-code resp) 201))
14
15 (test-case "handler raises on missing param"
16 (define req (make-mock-request))
17 (check-exn exn:fail?
18 (lambda () (post-param 'missing req)))))
Integration Testing with HTTP Client
1(module+ test
2 (require net/http-easy)
3
4 ; Start test server
5 (define stop-server (serve #:dispatch ... #:port 8089))
6 (sleep 0.1)
7
8 (test-case "POST request integration"
9 (define response
10 (post "http://localhost:8089/api/users"
11 #:data "username=john&[email protected]"
12 #:headers (hash 'content-type "application/x-www-form-urlencoded")))
13
14 (check-equal? (response-status-code response) 201)
15 (define body (bytes->string/utf-8 (response-body response)))
16 (check-true (string-contains? body "success")))
17
18 (stop-server))
Common Pitfalls in Racket Web Development
1. Dispatch-rules Returns Two Values
1; WRONG - only captures first value
2(define router (dispatch:dispatch-rules ...))
3
4; CORRECT - captures both dispatcher and request parser
5(define-values (route-dispatch request-parser)
6 (dispatch:dispatch-rules ...))
2. Stateless vs Stateful Servlets
1; Stateless (REST APIs) - no continuation URLs
2#:stateless? #t
3
4; Stateful (web apps with forms) - uses continuation URLs
5#:stateless? #f
Pattern: Always use #:stateless? #t for REST APIs and microservices.
3. Response Body Format
The response body must be a list of byte strings:
1; Helper to ensure correct format
2(define (make-response-body content)
3 (list (string->bytes/utf-8 content)))
4
5; Or trim and convert
6(define (xml-response-body xml-string)
7 (list (string->bytes/utf-8 (string-trim xml-string "\uFEFF")))) ; Remove BOM
4. Request Bindings Type Ambiguity
1; Documentation says extract-binding/single returns string,
2; but it may return bytes
3(define (safe-post-param name request)
4 (define value (extract-binding/single name (request-bindings request)))
5 (if (bytes? value)
6 (bytes->string/utf-8 value)
7 value))
5. Multiple Parameters with Same Name
1; extract-binding/single fails with multiple params
2; Use extract-bindings instead
3(define (get-all-params name request)
4 (extract-bindings name (request-bindings request)))
5
6; Returns list of values
7(define tags (get-all-params 'tag request)) ; => '("racket" "web" "tutorial")
Advanced Web Development Patterns
Custom Response Helper
1(define/contract (api-response data #:status [status 200])
2 (->* (any/c) (#:status number?) response?)
3 (define response-bytes (string->bytes/utf-8 (jsexpr->string data)))
4 (response/full
5 status
6 #f
7 (current-seconds)
8 #"application/json"
9 '()
10 (list response-bytes)))
Content Negotiation
1(define (negotiated-response request data)
2 (define accept-header
3 (header-value
4 (headers-assq #"Accept" (request-headers/raw request))))
5
6 (match accept-header
7 [#"application/json" (response/jsexpr data)]
8 [#"application/xml" (response/xexpr (hash->xexpr data))]
9 [_ (response/jsexpr data)])) ; Default to JSON
Rate Limiting Middleware
1(define rate-limits (make-hash)) ; IP -> (timestamp . count)
2
3(define (rate-limit-middleware request)
4 (define client-ip (request-client-ip request))
5 (define now (current-seconds))
6 (define limit-entry (hash-ref rate-limits client-ip (cons now 0)))
7 (define (timestamp . count) limit-entry)
8
9 (cond
10 [(> (- now timestamp) 60) ; Reset after 1 minute
11 (hash-set! rate-limits client-ip (cons now 1))
12 (route-dispatch request)]
13 [(>= count 100) ; Max 100 requests per minute
14 (response/jsexpr #:code 429
15 (hash 'error "Rate limit exceeded"))]
16 [else
17 (hash-set! rate-limits client-ip (cons timestamp (add1 count)))
18 (route-dispatch request)]))
Request Context Threading
1; Use parameters for request-scoped values
2(define current-user (make-parameter #f))
3(define current-request-id (make-parameter #f))
4
5(define (with-context-middleware request)
6 (parameterize ([current-request-id (generate-uuid)]
7 [current-user (authenticate request)])
8 (route-dispatch request)))
9
10; Access in handlers
11(define (protected-handler request)
12 (if (current-user)
13 (response/jsexpr (hash 'user (current-user)))
14 (response/jsexpr #:code 401 (hash 'error "Unauthorized"))))
Web Server Performance Optimization
1. Use Immutable Responses
Prefer response/jsexpr and response/xexpr which handle immutable data efficiently:
1; Efficient - uses immutable hash
2(response/jsexpr (hash 'a 1 'b 2))
3
4; Less efficient - mutable conversion
5(define h (make-hash))
6(hash-set! h 'a 1)
7(response/jsexpr h)
2. Stream Large Responses
1(define (streaming-response)
2 (response/output
3 (lambda (out)
4 (for ([i (in-range 1000000)])
5 (fprintf out "Line ~a\n" i)))))
3. Connection Pooling for External APIs
1(require net/http-easy)
2
3; Reuse HTTP connection pool
4(define client (http-easy-pool))
5
6(define (external-api-call)
7 (http-get "https://api.example.com/data" #:pool client))
Summary of Web Development Best Practices
- Always use
#:stateless? #tfor REST APIs - Wrap dispatch in error handler to catch all exceptions
- Log after response to capture status and timing
- Use
define-valuesfor dispatch-rules - Handle both bytes and strings from bindings
- Use
servenotserve/servletin tests - Delay bindings in mock requests
- Prefer
response/jsexprandresponse/xexprfor common formats - Test with both unit and integration tests
- Use parameters for request-scoped context