1 Introduction
Ratpack is a toolkit for creating web applications that run on the JVM. It provides a structure in which to write code that responds to HTTP requests. It is not a full stack framework so does not prescribe the use of any other particular framework for other web application concerns such as security, persistence, marshalling etc. There are however optional add on libraries that integrate specific tools, and integrating a new tool or framework is typically very simple.
Ratpack applications are not Java Servlet applications. They do not deploy to Java application servers. Ratpack builds on Netty, which is a high performance IO engine. Application deployments are self contained JVM applications.
Ratpack tries to strike the balance between provide helpful abstractions and infrastructure, while not being too prescriptive. It aims to make it easy to write small applications, yet not get in the way as your application evolves and becomes more complex
1.1 Highlights
Here are some key highlights:
- High throughput / Asynchronous IO
- Optimized for Java 8 & Groovy
- Dependency injection via Google Guice
- Development time support (reloading and build tooling)
Read on for details.
1.2 How to read the documentation
The canonical reference documentation for Ratpack is the Javadoc. This manual acts as a starting point for different aspects of Ratpack and as a mechanism to pull together larger ideas and patterns. The manual frequently introduces topics at a high level and links through to the Javadoc for detailed API information. Much work goes in to making the Javadoc (hopefully) useful.
1.3 Project Status
Ratpack is a new project, but stands on the shoulders of well established technologies and is built by developers with substantial experience in web application frameworks and tooling. It is currently pre 1.0 but under very active development.
No API compatibility guarantees are provided before the 1.0 release, from which point on the API will be strictly semantically versioned.
While it is new, it has received many man hours of development time.
2 Setup
Ratpack is purely a runtime. There is no installable package. To build Ratpack applications, you can use any JVM build tool. The Ratpack project provides specific support for Gradle through plugins, but any could be used.
Ratpack is published as a set of library JARs. The ratpack-core
library is the only strictly required library. Others such as ratpack-groovy
, ratpack-guice
, ratpack-jackson
, ratpack-test
etc. are optional.
With Ratpack on the classpath, you can use the API described in the next chapter to launch the application.
2.1 Using the Gradle plugin(s)
TODO
2.2 Using Lazybones project templates
Lazybones is a command line tool that allows you to generate a project structure for any framework based on pre-defined templates.
Ratpack’s Lazybones templates can be found on Bintray in the ratpack/lazybones repository. Templates are published with each Ratpack release and template versions are aligned with Ratpack release versions. There are different types of Ratpack templates available, listed in the repository. See the description of each for details.
See the Lazybones documentation for help with installing Lazybones.
Lazybones commands are in the format…
lazybones create <ratpack template> <ratpack version> <app name>
With Lazybones installed, creating a new Ratpack application is as easy as…
lazybones create ratpack my-ratpack-app
cd my-ratpack-app
./gradlew run
This will use the latest available version of Ratpack. If a specific version is required…
lazybones create ratpack 0.9.0 my-ratpack-app
cd my-ratpack-app
./gradlew run
3 Launching
This chapter introduces how to launch a Ratpack application, and the associated launch time configuration.
3.1 Launch configuration
To launch a Ratpack application, you must create a LaunchConfig
. This can then be used with the RatpackServerBuilder
method to create a RatpackServer
that can then be started.
Note that one of the LaunchConfig
object’s responsibilities is to provide a HandlerFactory
implementation. This factory is responsible for creating the handler that is effectively the Ratpack application. See the chapter on handlers for more details.
One option for building a LaunchConfig
is to use the LaunchConfigBuilder
. Another option is to use the LaunchConfigFactory
which is able to build a launch config from system properties and a properties file.
3.2 Main classes
Ratpack also provides some ready to use “main” classes that can be used to start the application. These main classes build on top of LaunchConfigFactory
.
The RatpackMain
is a bare bones entry point and is suitable for use in most cases.
The GroovyRatpackMain
entry point configures a Groovy based application to be launched.
4 Handlers
This chapter introduces handlers, which are the fundamental components of a Ratpack application.
4.1 What is a handler?
Conceptually, a handler (Handler
) is just a function that acts on a handling context (Context
).
The “hello world” handler looks like this…
import ratpack.handling.Handler;
import ratpack.handling.Context;
public class HelloWorld implements Handler {
public void handle(Context context) {
context.getResponse().send("Hello world!");
}
}
As we saw in the previous chapter, one of the mandatory launch config properties is the HandlerFactory
implementation that provides the primary handler. The handler that this factory creates is effectively the application.
This may seem limiting, until we recognise that a handler does not have to be an endpoint (i.e. it can do other things than generate a HTTP response). Handlers can also delegate to other handlers in a number of ways, serving more of a routing function. The fact that there is no framework level (i.e. type) distinction between a routing step and an endpoint offers much flexibility. The implication is that any kind of custom request processing pipeline can be built by composing handlers. This compositional approach is the canonical example of Ratpack’s philosophy of being a toolkit instead of a magical framework.
The rest of this chapter discusses aspects of handlers that are beyond HTTP level concerns (e.g. reading headers, sending responses etc.), which is addressed in the HTTP chapter.
4.2 Handler delegation
If a handler is not going to generate a response, it must delegate to another handler. It can either insert one or more handlers, or simply defer to the next handler.
Consider a handler that routes to one of two different handlers based on the request path. This can be implemented as…
import ratpack.handling.Handler;
import ratpack.handling.Context;
public class FooHandler implements Handler {
public void handle(Context context) {
context.getResponse().send("foo");
}
}
public class BarHandler implements Handler {
public void handle(Context context) {
context.getResponse().send("bar");
}
}
public class Router implements Handler {
private final Handler fooHandler = new FooHandler();
private final Handler barHandler = new BarHandler();
public void handle(Context context) {
String path = context.getRequest().getPath();
if (path.equals("foo")) {
context.insert(fooHandler);
} else if (path.equals("bar")) {
context.insert(barHandler);
} else {
context.next();
}
}
}
The key to delegation is the context.insert()
method that passes control to one or more linked handlers. The context.next()
method passes control to the next linked handler.
Consider the following…
import ratpack.handling.Handler;
import ratpack.handling.Context;
public class PrintThenNextHandler implements Handler {
private final String message;
public PrintThenNextHandler(String message) {
this.message = message;
}
public void handle(Context context) {
System.out.println(message);
context.next();
}
}
public class Application implements Handler {
public void handle(Context context) {
context.insert(
new PrintThenNextHandler("a"),
new PrintThenNextHandler("b"),
new PrintThenNextHandler("c")
);
}
}
Given that Application
is the primary handler (i.e. the one returned by the launch config’s HandlerFactory
), when this application receives a request the following will be written to System.out
…
a
b
c
And then what? What happens when the “c” handler delegates to its next? The last handler is always an internal handler that issues a HTTP 404 client error (via context.clientError(404)
which is discussed later).
Consider that inserted handlers can themselves insert more handlers…
import ratpack.handling.Handler;
import ratpack.handling.Context;
public class PrintThenInsertOrNextHandler implements Handler {
private final String message;
private final Handler[] handlers;
public PrintThenInsertOrNextHandler(String message, Handler... handlers) {
this.message = message;
this.handlers = handlers;
}
public void handle(Context context) {
System.out.println(message);
if (handlers.length == 0) {
context.next();
} else {
context.insert(handlers);
}
}
}
public class Application implements Handler {
public void handle(Context context) {
context.insert(
new PrintThenInsertOrNextHandler("a",
new PrintThenInsertOrNextHandler("a.1"),
new PrintThenInsertOrNextHandler("a.2"),
),
new PrintThenInsertOrNextHandler("b",
new PrintThenInsertOrNextHandler("b.1",
new PrintThenInsertOrNextHandler("b.1.1")
),
),
new PrintThenInsertOrNextHandler("c")
);
}
}
This would write the following to System.out
…
a
a.1
a.2
b
b.1
b.1.1
c
This demonstrates how the next handler of the handler that inserts the handlers becomes the next handler of the last of the inserted handlers. You might need to read that sentence more than once.
You should be able to see a certain nesting capability emerge. This is important for composibility, and also for scoping which will be important when considering the registry context later in the chapter.
It would be natural at this point to think that it looks like a lot of work to build a handler structure for a typical web application (i.e. one that dispatches requests matching certain request paths to endpoints). Read on.
4.3 Building handler chains
Ratpack provides a suite of routing type handlers out of the box that make it easy to compose dispatch logic. These are available via the static methods of the Handlers
class.
For example, the path(String, Handler)
method can be used for path based routing.
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.launch.LaunchConfig;
import ratpack.launch.HandlerFactory;
import static ratpack.handling.Handlers.path;
import static ratpack.handling.Handlers.get;
import static ratpack.handling.Handlers.chain;
public class SomeHandler implements Handler {
public void handle(Context context) {
// do some application work
}
}
public class Application implements HandlerFactory {
public Handler create(LaunchConfig launchConfig) {
return path("foo/bar", chain(get(), new SomeHandler()));
}
}
Here we have a HandlerFactory
that can be used when launching an app (see previous chapter). For this “application”:
- a GET request to
/foo/bar
would be routed to theSomeHandler
- a non-GET request to
/foo/bar
would produce a HTTP 405 (method not allowed) - anything else would produce a HTTP 404
This is easier than doing it all yourself, but we can do better. We can use the chain()
method and the Chain
DSL.
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.handling.Chain;
import ratpack.func.Action;
import ratpack.launch.LaunchConfig;
import ratpack.launch.HandlerFactory;
import static ratpack.handling.Handlers.chain;
public class Application implements HandlerFactory {
public Handler create(LaunchConfig launchConfig) {
return chain(launchConfig, new Action<Chain>() {
void execute(Chain chain) {
chain.
prefix("api", chain.chain(new Action<Chain>() {
void execute(Chain apiChain) {
apiChain.
delete("someResource", new Handler() {
public void handle(Context context) {
// delete the resource
}
});
}
})).
assets("public").
get("foo/bar", new Handler() {
public void handle(Context context) {
// do stuff
}
});
}
});
}
}
(note: the use of inner classes adds a lot of syntactic bloat here, things are more concise with Java 8 lambdas)
The chain DSL is built on the existing delegation methods that have been presented so far. It is merely syntactic sugar. The Groovy version of this DSL is extremely sweet…
import ratpack.handling.Handler
import ratpack.launch.LaunchConfig
import ratpack.launch.HandlerFactory
import static ratpack.groovy.Groovy.chain
class Application implements HandlerFactory {
Handler create(LaunchConfig launchConfig) {
chain(launchConfig) {
prefix("api") {
delete("someResource") {
// delete the resource
}
}
assets("public")
get("foo/bar") {
// do stuff
}
}
}
}
See the chapter on Groovy for more information on using Groovy with Ratpack.
5 Context
The Context
type is at the core of Ratpack.
It provides:
- Access the HTTP
Request
andResponse
- Delegation and flow control (via the
next()
andinsert()
methods) - Access to contextual objects
- Convenience for common handler operations
For working directly with the request/response, see the HTTP chapter.
For information about delegation, see the Handlers chapter.
5.1 Contextual objects
The context is a registry. It provides access to via-type-lookup of objects that were made available upstream in the handler pipeline. This is the mechanism for inter-handler collaboration in Ratpack.
Consider the following example:
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.handling.Chain;
import ratpack.func.Action;
import ratpack.launch.HandlerFactory;
import ratpack.launch.LaunchConfig;
import static ratpack.registry.Registries.registry
import static ratpack.handling.Handlers.chain;
// The api of some business object
interface Person {}
// Implementation
public class PersonImpl implements Person {
private final String id;
public PersonImpl(String id) {
this.id = id;
};
}
public class Application implements HandlerFactory {
public Handler create(LaunchConfig launchConfig) {
return chain(launchConfig, new Action<Chain>() {
void execute(Chain chain) {
chain.
prefix("person/:id", new Action<Chain>() {
void execute(Chain personChain) {
personChain.
handler(new Handler() {
public void handle(Context context) {
String id = context.getPathTokens().get("id"); // (1)
Person person = new PersonImpl(id);
context.next(registry(Person.class, person)); // (2)
}
}).
get("status", new Handler() {
public void handle(Context context) {
Person person = context.get(Person.class); // (3)
// show the person's status
}
}).
get("age", new Handler() {
public void handle(Context context) {
Person person = context.get(Person.class); // (4)
// show the person's age
}
});
}
});
}
});
}
}
At (2)
we are pushing the Person
instance into the registry for the downstream handlers to use and at (3)
and (4)
how they retrieve it. We are decoupling the creation detail from the usage, and avoiding duplicating the creation code in the status
and age
handlers. The benefit of avoiding duplication is obvious. What’s slightly more subtle is that the decoupling makes testing easier when the downstream handlers are not implemented as anonymous classes (see the Testing chapter for for information).
At (1)
we are also using contextual objects. The prefix()
chain method binds on a request path, potentially capturing tokens. If the binding is successful, a PathBinding
object is registered with the context that describes the binding result. This includes any path tokens that were captured as part of the binding. In the case above, we are capturing the second path component as the id
. The getPathTokens()
method on a context is literally shorthand for get(PathBinding.class).getPathTokens()
on the same context. This is another example of using the context object mechanism for inter-handler communication.
Another example of using contextual objects is the shorthand for accessing files from the file system. Consider the following script, which makes use of the context’s file
method to retrieve a static asset from the file system:
import static ratpack.groovy.Groovy.ratpack
ratpack {
handlers {
get {
def f = file('../')
render f ?: "null-value"
}
}
}
In the above example, the context’s file()
method is being called to retrieve a java.io.File
instance for the provided path. The context’s file()
method is a shorthand to retrieve the FileSystemBinding
object from the registry, and literally is a shorthand to get(FileSystemBinding.class).file(path/to/file)
. The context will always resolve file assets relative to the application root, so in the case where an absolute path is provided, it should be noted that the path to the asset will be prefixed by the path in which the application exists. For example, if your application exists in /home/ratpack/app
and your handler uses the file
method to resolve /etc/passwd
, then the actual path that is resolved will be /home/ratpack/app/etc/passwd
. In the case where a file cannot be resolved from within the application’s root, the file()
method may return a null value, which is demonstrated in the above example. The developer is responsible for handling scenarios where accessing a file may return a null object.
5.1.1 Partitioning
The context object mechanism supports partitioning application logic by providing different objects to different partitions. This is because objects registered with context are implicitly scoped, depending on how they were registered. Objects registered with the next()
methods are available to all downstream handlers that were part of the same insertion (i.e. context.insert()
including and nested insertions. Objects registered with the insert()
methods are available to the inserted handlers and nested insertions.
A typical use for this is using different error handling strategies for different parts of your application.
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.handling.Chain;
import ratpack.func.Action;
import ratpack.launch.HandlerFactory;
import ratpack.launch.LaunchConfig;
import ratpack.error.ServerErrorHandler;
import static ratpack.handling.Handlers.chain;
public class ApiHandlers implements Action<Chain> {
public void execute(Chain chain) {
// add api handlers
}
}
public class ApiServerErrorHandler implements ServerErrorHandler {
public void error(Context context, Exception exception) {
// return error data object
}
}
public class AppHandlers implements Action<Chain> {
public void execute(Chain chain) {
// add normal app handlers
}
}
public class AppServerErrorHandler implements ServerErrorHandler {
public void error(Context context, Exception exception) {
// display pretty error page
}
}
public class Application implements HandlerFactory {
public Handler create(final LaunchConfig launchConfig) {
return chain(launchConfig, new Action<Chain>() {
void execute(Chain chain) {
chain.
prefix("api", chain.chain(new Action<Chain>() {
void execute(Chain apiChain) {
apiChain.
register(ServerErrorHandler.class, new ApiServerErrorHandler(), new ApiHandlers());
}
})).
register(ServerErrorHandler.class, new AppServerErrorHandler(), new AppHandlers());
}
});
}
}
6 Basic HTTP
This chapter introduces how to deal with basic HTTP concerns such as parsing requests, rendering responses, content negotiation file uploads etc.
6.1 Request & Response
The context object that a handler operates on provides the (getRequest()
& getResponse()
methods for accessing the Request
and Response
respectively. These objects provide more or less what you would expect.
For example, they both provide a getHeaders()
method that returns an model of the HTTP headers sent with the request and a model of the HTTP headers that are to be sent with the response. The Request
exposes other metadata attributes such as the HTTP method, the URI and a key/value model of the query string parameters among other things.
6.2 Reading the request
TODO introduce parsers generally
6.2.1 Forms
Ratpack provides a parser for Form
objects in the core. This can be used for reading POST’d (or PUT’d etc. for that matter) forms, both URL encoded and multi part (including file uploads).
Here’s an example of using this from Java…
import ratpack.handling.Handler;
import ratpack.handling.Context;
import ratpack.form.Form;
import ratpack.form.UploadedFile;
public class MyHandler implements Handler {
public void handle(Context context) {
Form form = context.parse(Form.class);
// Get the first attribute sent with name “foo”
String foo = form.get("foo");
// Get all attributes sent with name “bar”
List<String> bar = form.getAll("bar");
// Get the file uploaded with name “myFile”
UploadedFile myFile = form.file("myFile");
// Send back a response …
}
}
See Form
and UploadedFile
for more information and examples.
6.3 Sending a response
TODO introduce status methods
TODO introduce send methods
TODO introduce renderers
TODO introduce sendFile methods (pointing to use of render(file(«path»)))
instead.
Cookies
TODO introduce getCookies() on request and response
TODO introduce Request#oneCookie()
TODO introduce Response#expireCookie()
6.5 Sessions
TODO introduce ratpack-sessions library
7 Backgrounding
By default, Ratpack is based on non blocking (or async) IO which is more efficient than blocking (or sync) IO based applications and therefore supports greater throughput. A key implication of this is that handlers must make special provisions if they do need to perform blocking IO.
Non blocking apps use a small numbers of threads to handle requests, roughly equivalent to the number of processing units available to the JVM running the application. This is possible because these request threads are (ideally) always doing work instead of waiting for IO or doing long computations. This means that wherever possible you should use libraries to access external resources (e.g. files, databases, web services) that support asynchronous IO. However, this is not always possible. For example, many database access libraries only support blocking on IO. In such cases, you can use Ratpack’s backgrounding support to perform the blocking operation off the request handling threads.
Ratpack can also be used to write blocking applications (i.e. using a large thread pool for handling requests). This can result is simpler application code, but greatly reduced throughput. See the “launching” chapter chapter for details on how to launch a Ratpack app in this mode.
7.1 The Background API
The context object offers the getBackground() method that returns a Background object. This gives access to the background thread pool for performing blocking or long operations. The background(Callable) method provides a more convenient entry point for this API.
8 Google Guice integration
The ratpack-guice
jar provides integration with Google Guice for modularisation and dependency injection. It is typically used in most Ratpack applications, but it is not a mandatory dependency. However, the ratpack-groovy
jar does depend on the Guice integration.
See the Guice package API documentation for detailed usage information.
8.1 Modules
Guice provides the concept of a module, which is a kind of recipe for providing objects. See Guice’s “Getting Started” documentation for details.
The ratpack-guice
library provides the ModuleRegistry
type for registering modules to be used.
8.2 Dependency injected handlers
The Guice integration gives you a means of decoupling the components of your application. You can factor out functionality into standalone (i.e. non Handler
) objects and use these objects from your handlers. This makes your code more maintainable and more testable. This is the standard “Dependency Injection” or “Inversion of Control” pattern.
The Guice class provides static handler()
factory methods for creating root handlers that are the basis of the application. These methods (the commonly used ones) expose Chain
instance that can be used to build the application’s handler chain. The instance exposed by these methods provide a registry (via the getRegistry()
) method that can be used to construct dependency injected handler instances.
See the documentation of the Guice class for example code.
8.3 Modules as plugins
Ratpack does not have a formal plugin system. However, reusable functionality can be packaged as Guice modules.
For example, that ratpack-jackson
library provides the JacksonModule
class which is a Guice module. To integrate Jackson into your Guice backed Ratpack application (e.g. for serializing objects as JSON), you simply use this module.
In Groovy script application this is as easy as:
import ratpack.jackson.JacksonModule
import static ratpack.jackson.Jackson.json
import static ratpack.groovy.Groovy.ratpack
ratpack {
modules {
register new JacksonModule()
}
handlers {
get("some-json") {
render json(user: 1) // will render '{user: 1}'
}
}
}
See the Guice package API documentation for more info on registering modules.
See the Jackson package API documentation for more info on using the Jackson integration.
8.4 Guice and the context registry
TODO guice backed registry impl
This offers an alternative to dependency injected handlers, as objects can just be retrieved on demand from the context.
More usefully, this means that Ratpack infrastructure can be integrated via Guice modules. For example, an implementation of the ServerErrorHandler
can be provided by a Guice module. Because Guice bound objects are integrated into the context registry lookup mechanism, this implementation will participate in the error handling infrastructure.
This is true of all Ratpack infrastructure that works via context registry lookup, such as Renderer implementations for example.
9 Testing Ratpack applications
Ratpack provides testing support, both at the functional and unit level. The ratpack-test
library contains the core support, and ratpack-test-groovy
provides convenient Groovy extensions.
Note: The ratpack
and ratpack-groovy
Gradle plugins auto configure these libraries to be added to the test classpath.
9.1 Unit testing
The primary integration point between Ratpack and your code is the Handler
contract. Ratpack provides the UnitTest.invoke()
and GroovyUnitTest.invoke()
for contriving the invocation of a handler. As the routing of requests is also implemented via handlers in Ratpack, this can also be used to test the routing.
Ratpack doesn’t couple to any particular test framework. It only provides some utilities that you can use from whatever test framework you choose. However, we strongly recommend using the Groovy based Spock Framework, even if your application is implemented in Java.
There is no special setup or teardown required for unit testing Ratpack applications, as by definition they require no server infrastructure.
9.2 Functional testing
Functional testing for Ratpack is built up around the ApplicationUnderTest.
This interface provides the address of the running application. Implementations of this interface will take care of starting the server for you.
9.2.1 TestHttpClient
Ratpack provides TestHttpClient in ratpack-test-groovy
, this is a client that makes it very simple to test status codes and responses.
Note below we use @Delegate so we just need to call get()
in the when block instead of client.get()
.
import ratpack.groovy.test.LocalScriptApplicationUnderTest
import ratpack.groovy.test.TestHttpClient
import ratpack.groovy.test.TestHttpClients
import ratpack.test.ApplicationUnderTest
class SiteSmokeSpec {
LocalScriptApplicationUnderTest aut = new LocalScriptApplicationUnderTest()
@Delegate TestHttpClient client = TestHttpClients.testHttpClient(aut)
def "Check Site Index"() {
get("index.html")
assert response.statusCode == 200
assert response.body.asString().contains('<title>Ratpack: A toolkit for JVM web applications</title>')
}
def "Check Site Root"() {
get()
assert response.statusCode == 200
assert response.body.asString().contains('<title>Ratpack: A toolkit for JVM web applications</title>')
}
def cleanup() {
aut.stop()
}
}
9.2.2 Geb
Geb can also be used we just need to set up the correct base URL and make sure the test app is shut down.
To set the correct base URL we will use the ServerBackedApplicationUnderTest instance to get the address and give that to the Geb browser instance.
For shutting down the app we will call stop in the cleanup function.
An example of a Geb based test is available here.
9.2.3 Application subsets, modules and extensions
The ratpack.test.embed
package (provided by the ratpack-test
library) provides the EmbeddedApplication
interface and implementations. These can be used to functionally test features in isolation, by creating small Ratpack apps within test cases and is actually the basis for how Ratpack itself is tested. This approach is most commonly used for functionally testing reusable Ratpack components (e.g. Guice modules) but can also be used for functionally testing a subset of an application.
The LaunchConfigEmbeddedApplication
is a convenient abstract super class, where implementors only need to provide a LaunchConfig
to define the application to test. The ratpack.groovy.test.embed
package (provided by the ratpack-groovy-test
library) provides the ClosureBackedEmbeddedApplication
implementation that uses user supplied closures as the basis of the application to test. This is the preferred implementation to use as it provides Guice support, flexibility and is easy to use.
The EmbeddedApplication
type extends the ApplicationUnderTest
type. This makes them convenient to use with the TestHttpClient
mechanism provided by the ratpack-groovy-test
library.
10 Groovy
TODO
11 The Ratpack project
11.1 Credits
Ratpack has been made possible by the following people.
11.1.1 Active project members
The following people are currently actively contributing their time to Ratpack.
11.1.2 Contributors
The following people have provided significant contributions.
- Tim Berglund
- Peter Ledbrook
- Marco Vermeulen
- Jeff Beck
- Paul Fairless
- Tomás Lin
- Kevin Marquardsen
- Stefano Gualdi
- Ben Navetta
- Bobby Warner
- Jim White
- Dimitris Zavaliadis
- David M. Carr
11.1.3 Past project members
The following people have contributed their time to Ratpack in the past, but are no longer actively working on it.
11.2 About this manual
11.2.1 Resources
11.2.1.1 Libraries
- Prism by Lea Verou
- Normalize.css by Nicolas Gallagher
- Modernizr
- Breakpoint by Mason Wendell and Sam Richard
11.2.1.2 Fonts
Web fonts are served by Google Web Fonts.
- Merriweather and Merriweather Sans by Eben Sorkin
- Engagement by Astigmatic One Eye Typographic Institute
11.2.1.3 Images
- Hat designed by Adhara Garcia from The Noun Project
- Microphone designed by Cris Dobbins from The Noun Project
- Whisky designed by Anthony Lui from The Noun Project
- Retro starburst 3 from Velma’s Retro Clip Art and Images (last known URL: http://free-retro-graphics.com/2011/02/retro-starburst-3/)
- Jagged Edge social media icons from Geek Fairy