AF-BedSheetUser Guide


BedSheet is a Fantom framework for delivering web applications. Built on top of afIoc and Wisp, BedSheet aims to be: Powerful, Flexible and Simple.

BedSheet is inspired by Java's Tapestry5, Ruby's Sinatra and Fantom's Draft.

Quick Start

  1. Create an AppModule, this is where all your service configuration will go.
  2. Contribute to Routes and other services
  3. Create some page / request handlers
  4. Start the app...
using afBedSheet
using afIoc

class AppModule {
  static Void contributeRoutes(OrderedConfig conf) {
    conf.add(Route(`/hello/**`, HelloPage#hello))

class HelloPage {
  TextResponse hello(Str name, Int iq := 666) {
    return TextResponse.fromPlain("Hello! I'm $name and I have an IQ of $iq!")

From the command line:

$ fan afBedSheet <mypod>::AppModule 8080
BedSheet v1.0 started up in 323ms

$ curl http://localhost:8080/hello/Traci/69
Hello! I'm Traci and I have an IQ of 69!

$ curl http://localhost:8080/hello/Luci
Hello! I'm Luci and I have an IQ of 666!

Wow! That's awesome! But what just happened!?

Every application has an AppModule that is used to contribute to afIoc services. In our AppModule we told the Routes service to route all request uris, that start with /hello, to our HelloPage class. Route converts any extra uri path segments into method arguments, or in our case, a Str name and an optional Int iq.

Request handlers are typically what we, the app developers, write. They perform logic processing and render responses. Our HelloPage handler simply renders a TextResponse.

A default ResponseProcessor then sends our TextResponse to the client.

Starting BedSheet

BedSheet may be started from the command line using:

$ fan afBedSheet <fully-qualified-app-module-name> <port-number>

Every BedSheet web application has an AppModule class that defines and configures your afIoc services. It is an afIoC concept that allows you centralise your app configuration in one place.

TIP: Should your AppModule grow too big, break logical chunks out into their own classes using the afIoc @SubModule facet.

You may find it more convenient to create your own BedSheet Main wrapper. (In fact, if using Heroku you have to.)

using util

class Main : AbstractMain {

  @Arg { help="The HTTP port to run the app on" }
  private Int port

  override Int run() {
    afBedSheet::Main().main("<fully-qualified-app-module-name> $port".split)
    return 0

<fully-qualified-app-module-name> may be replaced with <pod-name> as long as your pod's defines the following meta:

meta  = [ ...
          "afIoc.module"  : "<fully-qualified-app-module-name>"

Note that AppModule is named so out of convention but may be called anything you like.

Request Routing

When BedSheet receives a web request, it is matched to a handler for processing. This is configured through the Routes service.

@Contribute { serviceType=Routes# }
static Void contributeRoutes(OrderedConfig conf) {

  conf.add(Route(`/index`, IndexPage#service))

BedSheet is bundled with the following Route objs:

  • Route: Matches against the request URI, converting deeper path segments into method arguments.

If the handler returns Void or null then processing is passed onto the next matching route. This makes it possible to create filter like handlers that pre-process a blanket wide set of uris. To do this, contribute a placeholder that all routes (ordered or unordered) naturally come after. Example:

@Contribute { serviceType=Routes# }
static Void contributeRoutes(OrderedConfig conf) {


  // routes
  conf.add(Route(`/index`, IndexPage#service))

  // filters - addOrdered to ensure they come before routes
  conf.addOrdered("AuthFilter", Route(`/admin/***`, AuthFilter#service), ["before: routes"])

Routes are not limited to matching against uris, you can create your own routes that match against anything, such as http headers or the time of day! Just create a RouteMatcher and contribute it to RouteMatchers.

Routing lesson over.

( Aussies may stop giggling now.)

Request Handling

Request handlers are where logic is processed and responses are rendered. Handlers generally shouldn't pipe anything to Response.out. Instead they should return a response object. Example: The above HelloPage handler returns a TextResponse.

BedSheet bundles with the following request handlers:

  • FileHandler: Maps request URIs to files on file system.

By default, BedSheet handles the following handler responses:

  • Void / null / false : Processing should fall through to the next Route match.
  • true : A response has been sent to the client and no further processing should performed on the http request.
  • File : The file (on the file system) is sent to the client.
  • TextResponse : The text (be it plain, json or xml), with the given MimeType is sent to the client.

Response Processing

ResponseProcessors process request handler responses (File, TextResponse, etc...) and send data to the client. ResponseProcessors should return true if no further processing should performed on the http request. Or they may return another response object for further processing, such as a TextResponse.

BedSheet bundles with the following response processors:

  • TextResponseProcessor for sending text to the client
  • FileResponseProcessor for sending files (on the file system) to the client

Error Processing

doc 404, 500 pages and Err handling

BedSheet Dev Proxy

Never (manually) restart your app again! Use the Dev Proxy to auto re-start your app when your pod changes:

$ fan afBedSheet -proxy <mypod> 8080

(Goes a step further than Draft 1.0.2 as) The real app dies if the proxy dies! No more lingering Fantom / Java processes.

docApp Proxy Restarter


By default, BedSheet compresses HTTP responses where it can.(1) But it doesn't do this willy nilly, oh no! There are many hurdles to overcome...

Disable All

Gzip, although enabled by default, can be disabled for the entire web app by setting the following config property:

config.addOverride(ConfigIds.gzipDisabled, "my.gzip.disabled", true)

Disable per Response

Although enabled by default, gzip can be disabled on a per request / response basis by calling the following:


Gzip'able Mime Types

Not everything should be gzipped. For example, text files gzip very well and yield high compression rates. JPG images on the other hand, because they're already compressed, don't gzip well and can end up bigger than the original! For this reason you must contribute to the GzipCompressible service to enable gzip for specified Mime Types:

config["text/funky"] = true

(Note: The GzipCompressible contrib type is actually MimeType - afIoc kindly coerces the Str to MimeType for us.)

By default BedSheet will compress plain text, css, html, javascript, xml, json and other text responses.

Gzip only when asked

It's guaranteed that someone, somewhere is still using Internet Explorer 3.0 and they can't handle gzipped content. As such, and as per RFC 2616 HTTP1.1 Sec14.3, we only gzip the response if the client actually asked for it!

Minimum content threshold

Gzip is great when compressing large files, but if you've only got a few bytes to squash... then the compressed version is going to be bigger! Which kinda defeats the point of using gzip in the first place! For that reason the response data must reach a minimum size / threshold before it gets gzipped. Set the threshold config with the following ApplicationDefaults:

config[ConfigIds.gzipThreshold] = 768

See GzipOutStream and ConfigIds.gzipThreshold for more details.

Phew! Made it!

If (and only if!) your data passed all the tests above, then it will be lovingly gzipped and sent to the client.

Buffered Response

By default, BedSheet sets the Content-Length http header in the http response.

doc Buffered Response


All request handlers and processors are built by afIoc so feel free to @Inject DAOs and other services. BedSheet itself is built with afIoc so look at the BedSheet source for afIoc examples.

TIP: const handler classes are cached by BedSheet and reused on every request.

There's also some Err reporting, HTTP status handling and more besides. It's early days still...

doc Internet Explorer Ajax Cache Buster

doc CORS

Push To Live!

In a hurry to go live?

Check out Heroku and see how ridiculously easy it is to deploy your app to a live server with the heroku-fantom-buildpack.

Release Notes

Breaking Changes


  • Route URIs now need the suffix ** to match all remaiing method arguments
  • TextResult has been renamed to TextRepsonse
  • MoustacheSource has been renamed to MoustacheTemplates
  • All Services ending in Source have renamed to their plural. Example: ValueEncoderSource -> ValueEncoders