At its core, Ratpack enables asynchronous, stateless HTTP applications. It is built on Netty, the event-driven networking engine. Unlike some web frameworks, there is no expectation that one thread handles one request. Instead, you are encouraged to handle blocking operations in a way that frees the current thread, thus allowing high performance.
Ratpack can be used to make responsive, RESTful microservices, although it’s not a requirement.
Unlike Grails and other popular web frameworks, Ratpack aims not to be a framework, but instead a set of libraries. Although it’s a lean set of libraries, Ratpack comes packed with support for JSON, websockets, SSE (server sent events), SSL, SQL, logging, Dropwizard Metrics, newrelic, health checks, hystrix, and more.
Ratpack uses Guice by default for DI
(dependency injection).
Script
In its simplest form, you can create a Ratpack application using only a
Groovy script. For example, the following script would run a simple Ratpack application that always responds “Hello World!”:
1 @Grab('io.ratpack:ratpack-groovy:1.6.1')
2
3 import static ratpack.groovy.Groovy.ratpack
4
5 ratpack {
6 handlers {
7 handler {
8 response.send "Hello World!"
9 }
10 }
11 }
Gradle
For production systems you should use a Gradle build. Ratpack has its own
Gradle plugin that you can use, as follows (
jcenter refers to the Maven central alternative, Bintray’s JCenter):
1 buildscript {
2 repositories {
3 jcenter()
4 }
5 dependencies {
6 classpath 'io.ratpack:ratpack-gradle:1.6.1'
7 }
8 }
9
10 apply plugin: 'io.ratpack.ratpack-groovy'
11
12 repositories {
13 jcenter()
14 }
Using the ratpack-gradle plugin, you can run tasks, such as distZip, distTar, installApp, and run, which creates a ZIP distribution file, creates a TAR distribution file, installs the Ratpack application locally, and runs the application, respectively.
The
run task is very useful for test
driving your application. After invoking
gradle run, you should see the following output:
1 [main] INFO ratpack.server.RatpackServer - Starting server...
2 [main] INFO ratpack.server.RatpackServer - Building registry...
3 [main] INFO ratpack.server.RatpackServer - Ratpack started (development)
4 for http://localhost:5050
Ratpack Layout
Unlike in
Grails, you need to create these files and directories yourself (or use Lazybones).
Handlers
Handlers are the basic building blocks for Ratpack. They form something like a pipeline or “chain of responsibility.”
Multiple handlers can be called per request, but one must return a response. If none of your handlers is matched, a default handler returns a 404 status code.
1 import static ratpack.groovy.Groovy.ratpack
2
3 ratpack {
4 handlers {
5 all() { response.headers.add('x-custom', 'x'); next() }
6 get("foo") {
7 render "foo handler"
8 }
9 get(":key") {
10 def key = pathTokens.key
11 render "{"key": "$key"}"
12 }
13 files { dir "public" }
14 }
15 }
The first handler all() is used for every HTTP request and adds a custom header. It then calls next() so the next matching handler gets called. The next handler that matches a given HTTP request will be used to fulfill the request with a response.
The pathTokens map contains all parameters passed in the URL as specified in the matching path pattern, as in :key, by prefixing : to a variable name you specify. For example, if the preceding application is run, requesting /abc will get you the response {"key":"abc"}.
You can define a
handler using a method corresponding to any one of the HTTP methods:
Get
Post
Put
Delete
Patch
Options
To accept multiple methods on the same path, you should use the
path handler
and
byMethod with each method handler inside it. For example:
1 path("foo") {
2 byMethod {
3 post() { render 'post foo'}
4 put() { render 'put foo'}
5 delete() { render 'delete foo'}
6 }
7 }
Rendering
There are many ways to render a response. The “grooviest” ways are using the groovyMarkupTemplate or groovyTemplate methods
.
For example, here’s how to use the
groovyMarkupTemplate
:
1 import ratpack.groovy.template.MarkupTemplateModule
2 import static ratpack.groovy.Groovy.groovyMarkupTemplate
3 import static ratpack.groovy.Groovy.ratpack
4 ratpack {
5 bindings {
6 module MarkupTemplateModule
7 }
8 handlers {
9 get(":key") {
10 def key = pathTokens.key
11 render groovyMarkupTemplate("index.gtpl", title: "$key")
12 }
13 files { dir "public" }
14 }
15 }
This allows you to use the Groovy markup language, and by default it looks in the
ratpack/templates directory
for markup files. For example, your
index.gtpl might look like the following:
1 yieldUnescaped '<!DOCTYPE html>'
2 html {
3 head {
4 meta(charset:'utf-8')
5 title("Ratpack: $title")
6 meta(name: 'apple-mobile-web-app-title', content: 'Ratpack')
7 meta(name: 'description', content: “)
8 meta(name: 'viewport', content: 'width=device-width, initial-scale=1')
9 link(href: '/images/favicon.ico', rel: 'shortcut icon')
10 }
11 body {
12 header {
13 h1 'Ratpack'
14 p 'Simple, lean & powerful HTTP apps'
15 }
16 section {
17 h2 title
18 p 'This is the main page for your Ratpack app.'
19 }
20 }
21 }
- 1.
yieldUnescaped outputs
the given String directly into the resulting HTML.
- 2.
The Groovy markup language is a Groovy-based DSL to create HTML from Groovy code.
Groovy Text
If you prefer to use a plain old text document with embedded Groovy (much like a GString), you can use the
TextTemplateModule
:
1 import ratpack.groovy.template.TextTemplateModule
2 import static ratpack.groovy.Groovy.groovyTemplate
3 import static ratpack.groovy.Groovy.ratpack
4 ratpack {
5 bindings {
6 module TextTemplateModule
7 }
8 handlers {
9 get(":key") {
10 def key = pathTokens.key
11 render groovyTemplate("index.html", title: "$key")
12 }
13 }
14 }
Then create a file named
index.html in the
src/main/resources/templates directory with the following content:
1 <html><h1>${model.title}</h1></html>
It supplies a model map to your template, which contains all of the parameters you supply.
Handlebars and Thymeleaf
Ratpack also supports Handlebars and Thymeleaf templates, two alternative methods of generating dynamic web pages. Since these are static resource, you should put these templates in the src/main/resources directory.
You will first need to include the appropriate Ratpack project in your Gradle build file:
1 runtime 'io.ratpack:ratpack-handlebars:1.6.1'
2 runtime 'io.ratpack:ratpack-thymeleaf3:1.6.1'
To use Handlebars (an alternative template format), include the
HandlebarsModule and render Handlebar templates as follows:
1 import ratpack.handlebars.HandlebarsModule
2 import static ratpack.handlebars.Template.handlebarsTemplate
3 import static ratpack.groovy.Groovy.ratpack
4 ratpack {
5 bindings {
6 module HandlebarsModule
7 }
8 handlers {
9 get("foo") {
10 render handlebarsTemplate('myTemplate.html', title: 'Handlebars')
11 }
12 }
13 }
Create a file named
myTemplate.html.hbs (.hbs is the Handlebars suffix) in the
src/main/resources/handlebars directory
with Handlebar content. For example:
Thymeleaf works in a similar way:
1 import ratpack.thymeleaf.ThymeleafModule
2 import static ratpack.thymeleaf.Template.thymeleafTemplate
3 import static ratpack.groovy.Groovy.ratpack
4 ratpack {
5 bindings {
6 module ThymeleafModule
7 }
8 handlers {
9 get("foo") {
10 render thymeleafTemplate('myTemplate', title: 'Thymeleaf')
11 }
12 }
13 }
The requested file should be named
myTemplate.html and be located in the
src/main/resources/thymeleaf directory
of your project. Example content:
1 <span th:text="${title}"/>
JSON
Integration with the Jackson JSON marshaling library provides the ability to work with JSON.
The
ratpack.jackson.Jackson class provides most of the Jackson-related functionality. For example, to render JSON, you can use
Jackson.json:
1 import static ratpack.jackson.Jackson.json
2 ratpack {
3 bindings {
4 }
5 handlers {
6 get("user") {
7 render json([user: 1])
8 }
9 }
10 }
The Jackson integration also includes a parser for converting JSON request bodies into objects. The Jackson.jsonNode() and Jackson.fromJson(Class) methods can be used to create parseable objects to be used with the parse() method.
For example, the following handler would parse a JSON String representing a
Person object
and render the
Person’s name:
1 post("personNames") {
2 render( parse(fromJson(Person.class)).map {it.name} )
3 }
Bindings
Bindings in Ratpack make objects available to the handlers. If you are familiar with Spring, Ratpack uses a registry, which is similar to the application context in Spring. It can also be thought of as a simple map from classtypes to instances. Ratpack-Groovy uses Guice by default, although other direct-injection frameworks, such as Spring, can be used (or none at all).
Instead of a formal plugin system, reusable functionality can be packaged as modules. You can create your own modules to properly decouple and organize your application into components. For example, you might want to create a MongoModule or a JdbcModule
. Alternatively, you could break your application into services. Either way, the registry is where you put them.
Anything in the registry can be automatically used in a handler and gets wired in by
classtype from the registry. Here’s an example of bindings in action:
1 bindings {
2 bindInstance(MongoModule, new MongoModule())
3 bind(DragonService, DefaultDragonService)
4 }
5 handlers {
6 get('dragons') { DragonService dService ->
7 dService.list().then { dragons ->
8 render(toJson(dragons))
9 }
10 }
11 }
The system will automatically get the DragonService defined in the bindings when referenced as a parameter to a closure like dService.
Blocking
Blocking operations should be handled by using the blocking API. A blocking operation is anything that is IO-bound, such as querying the database.
The
Blocking class is located at
ratpack.exec.Blocking. For example, the following handler calls a method on the database (which is defined outside of the scope of this example):
1 get("deleteOlderThan/:days") {
2 int days = pathTokens.days as int
3 Blocking.get { database.deleteOlderThan(days) }
4 .then { int i -> render("$i records deleted") }
5 }
You can chain multiple blocking calls using the then method. The result of the previous closure is passed as the parameter to the next closure (an int in the preceding handler).
Ratpack handles the thread scheduling for you and then joins with the original computation thread. This way, you can rejoin with the original HTTP request thread and return a result.
1 get("deleteOlderThan/:days") {
2 int days = pathTokens.days as int
3 int result
4 Blocking.get { database.deleteOlderThan(days) }
5 .then { int count -> result = count }
6 render("$result records deleted")
7 }
If no return value is required, use the Blocking.exec method.
Configuration
Any self-respecting web application should allow configuration to come from multiple locations: the application, the filesystem, environment variables, and system properties. This is a good practice in general, but especially for cloud-native apps.
Ratpack includes a built-in configuration API. The interface ratpack.config.ConfigData and its methods allow you to layer multiple sources of configuration. Using the of method with a passed closure allows you to define a factory of sorts for configuration from JSON, YAML, and other sources.
First, you define your configuration classes. These classes’ properties will define the names of your configuration properties. For example:
1 class Config {
2 DatabaseConfig database
3 }
4 class DatabaseConfig {
5 String username = "root"
6 String password = ""
7 String hostname = "localhost"
8 String database = "myDb"
9 }
In this case, your JSON configuration might look like the following:
1 {
2 "database": {
3 "username": "user",
4 "password": "changeme",
5 "hostname": "myapp.dev.company.com"
6 }
7 }
Later, in the
binding declaration
, add the following to bind the configuration defined by the previous classes:
1 def configData = ConfigData.of { d -> d.
2 json(getResource("/config.json")).
3 yaml(getResource("/config.yml")).
4 sysProps().
5 env().build()
6 }
7 bindInstance(configData.get(Config))
Each declaration overrides the previous declarations. So in this case, the order would be class definition, config.json, config.yml, system properties, and then environment variables. This way you could override properties at runtime.
System property and environment variable configuration must be prefixed with the ratpack. and RATPACK_ prefixes. For example, the hostname property
would be ratpack.database.hostname as a system property or RATPACK_DATABASE_HOSTNAME as environment variable.
Testing
Ratpack includes many test fixtures for aiding your unit, functional, and integration tests. It assumes you’ll be using Spock to test your application.
First, if you’re using the Ratpack-Gradle plugin, simply add the following dependencies:
1 dependencies {
2 testCompile ratpack.dependency('test')
3 testCompile "org.spockframework:spock-core:1.3-groovy-2.5"
4 testCompile 'cglib:cglib:3.2.12'
5 testCompile 'org.objenesis:objenesis:3.0.1'
6 }
The cglib and objenesis dependencies are needed for class and final class mocking.
Second, add your Spock tests under the
src/test/groovy directory using the same package structure as your main project. A simple test might look like the following:
1 package myapp.services
2 import spock.lang.Specification
3
4 class MyServiceSpec extends Specification {
5 void "default service should return Hello World"() {
6 setup:
7 "Set up the service for testing"
8 def service = new MyService()
9 when:
10 "Perform the service call"
11 def result = service.doStuff()
12 then:
13 "Ensure that the service call returned the proper result"
14 result == "Hello World"
15 "Shutdown the service when this feature is complete"
16 service.shutdown()
17 }
18 }
Ratpack enables your functional
tests by running your full application within a test environment using
GroovyRatpackMainApplicationUnderTest. For example, the following test would test the text rendered by your default handler:
1 package myapp
2 import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
3 import spock.lang.Specification
4
5 class FunctionalSpec extends Specification {
6 void "default handler should render Hello World"() {
7 setup:
8 def aut = new GroovyRatpackMainApplicationUnderTest()
9 when:
10 def response = aut.httpClient.text
11 then:
12 response == "Hello World!"
13 cleanup:
14 aut.close()
15 }
16 }
Merely calling
text on the
httpClient invokes a
GET request. However, more complex requests can be invoked using
requestSpec
. For example, to test a specific response is returned based on the
User-Agent header:
1 void "should properly render for v2.0 clients"() {
2 when:
3 def response = aut.httpClient.requestSpec { spec ->
4 spec.headers.'User-Agent' = ["Client v2.0"]
5 }.get("api").body.text
6 then:
7 response == "V2 Model"
8 }
Summary
This chapter taught you about the following:
How to get started with Ratpack
Using handlers
How to use bindings as a plugin architecture and for decoupling your modules
How to render using Groovy Markup, Groovy Text, Thymeleaf, and Handlebars templates
How to render and parse JSON
Doing blocking operations
Configuring a Ratpack app
Testing a Ratpack app