- [Getting Started](#Getting Started)
- Routing
- Params
- Request
- Controllers
- Templates
- Futures
- Responses
- Assets
- Headers
- Cookies
- Uploads
- Filters
- Logging
- Stats
- Testing
- Deploying
- Configs
Minimal app:
import io.peregrine._
object WebApp extends PeregrineApp {
get("/hi") { req =>
"Hello World!"
}
}
Install dependency in build.sbt file:
scalaVersion := "2.11.7"
resolvers += "Twitter" at "http://maven.twttr.com"
libraryDependencies += "com.github.dvarelap" %% "peregrine" % "1.2.2"
And run with:
$ sbt run
View at: http://localhost:5000/hi
A route is a HTTP method paired with a URL matching pattern. When Peregrine receives a request for a particular URL, it will scan all registered routes and dispatch to the first one that contains a match.
get("/") { req =>
// ...
}
delete("/") { req =>
// ...
}
post("/") { req =>
// ...
}
put("/") { req =>
// ...
}
head("/") { req =>
// ...
}
patch("/") { req =>
// ...
}
options("/") { req =>
// ...
}
It's also possible render a route from another route, like:
get("/") { request =>
route.get("/home")
}
get("/home") { request =>
...
}
And you can define multiple routes for a single action like:
// GET /home & GET /inicio will do the exact same action
get("/home", "/inicio") { request =>
...
}
You can even pass params through:
get("/dog-search") { request =>
route.post("/search", Map("q" -> "dogs"))
}
Customize what happens when a route isn't found using notFound
:
notFound { request =>
status(404).plain("not found yo")
}
Or, what happens when exceptions occur with error:
class Unauthorized extends Exception
error { request =>
request.error match {
case Some(e:ArithmeticException) =>
status(500).plain("whoops, divide by zero!")
case Some(e:Unauthorized) =>
status(401).plain("Not Authorized!")
case Some(e:UnsupportedMediaType) =>
status(415).plain("Unsupported Media Type!")
case _ =>
status(500).plain("Something went wrong!")
}
}
get("/secret") { request =>
throw(new Unauthorized)
}
Query parameters are supported through request.params
. This supports all the usual Map
methods you are used to, like getOrElse
:
get("/search") { request =>
val query = request.params.getOrElse("q", "dogs")
"you searched for " + query
}
Parameters can also be extracted from routes just like in Sinatra. These are stored in request.routeParams
:
get("/hello/:name") { request =>
val name = request.routeParams.getOrElse("name", "john doe")
"you searced for " + query
}
And a most generic way to extract params is via param
method
case class Person(firstName: String, lastName: String)
// GET /hello/Dan?lastName=Varela
get("/hello/:firstName") { req =>
val u = for {
firstName <- req.param("firstName")
lastName <- req.param("lastName")
} yield Person(firstName, lastName)
json(u)
}
You'll notice a request
object is passed into your routing code, this has useful information about the request:
get("/request-info") { request =>
println(request.remoteAddress)
println(request.path)
println(request.userAgent)
"done"
}
If you need to organize the actions in different files you can extends Controller
instead:
class UsersController extends Controller {
get("/") { request =>
// ...
}
post("/") { request =>
// ...
}
delete("/:id") { request =>
// ...
}
}
and then register it in the PeregringeApp
class MyServer extends PeregrineApp {
val usersController = new UsersController()
register(usersController)
}
You can call register
multiple times to register various controllers.
You can also define a prefix for each controller you register:
class UsersController extends Controller {
get("/") { request =>
// ...
}
}
class CompanyController extends Controller {
get("/") { request =>
// ...
}
}
object MyPrefixServer extends PeregrineApp {
register(new UsersController, "/users") // will respond on GET /users/
register(new CompanyController, "/companies") // will respond on GET /companies/
}
Peregrine supports mustache templates system. By default, this will look into views
folder for the template resources
you have a template like:
<!-- views/user.mustache -->
<h1>Hello {{model.name}} you're {{model.age}}</h1>
and render it using the method mustache
:
User(name: String, age: Int)
get("/view") { req =>
mustache("user", User("Matt", 16))
}
this will output:
<h1>Hello Matt you're 16</h1>
As you can see peregrine exposes the value with the name model
within the templates, so it's possible to
access those values using {{model.name}}
Note: Mustache is natively supported through Mustache.java.
Every route is expected to a return a Future[Response]
, the framework is prepared to receive a Future[ResponseBuilder]
or a ResponseBuilder
that'll be wrapped in a constant future. This is an important distinction from synchronous frameworks as all your routes may be executed concurrently instead of one at a time.
so in the following example both will result in a correct Future[ResponseBuilder]:
get("/explicitly") { req =>
render.plain("explicitly call to toFuture").toFuture // returns Future[ResponseBuilder]
}
get("/explicitly") { req =>
"no toFuture call" // returns Future[ResponseBuilder]
}
This is especially useful when dealing with libraries/services that return Future
's themselves (like a finagle-http client):
get("/current-time") { request =>
// returns a Future[ResponseBuilder]
httpClient.apply("/api/time.txt") map { response =>
val currentTime = response.contentString()
"the time is: " + currentTime
}
}
Note that we did not use toFuture
above because we are already within a Future
.
See Concurrent Programming with Futures for more details.
By default peregrine tries to render your message if you don't explicitly define how should this be done
get("/render-string-explicitly") { req =>
render.plain("hi!") // will output plain "hi!" with status 200
}
get("/render-string") { req =>
"hi!" // will also output plain "hi!" with status 200
}
The render
object is a powerful Response
builder that allows customizing the response in various ways:
get("/i-want-json") { request =>
json(Map("foo" -> "bar")) // will render json map
}
This will automatically set the Content-Type
as application/json
.
get("/i-want-html") { request =>
html("<h1>hi</h1>") // will render html code
}
Like the example above, this sets Content-Type
to text/html
. We can also set it to whatever we want:
get("/i-want-html") { request =>
body("custom response").contentType("application/mine")
}
If you want to extra data and because render
it's a builder, you can chain the methods in any order. Let's add a 201
to that response:
get("/i-want-html") { request =>
render.body("custom response")
.contentType("application/mine")
.status(201)
}
This is the same as:
get("/i-want-custom") { request =>
render.status(201)
.contentType("application/mine")
.body("custom response")
}
Sending a byte array:
get("/i-want-binary") { request =>
render.status(201)
.contentType("application/octet-stream")
.body(Array[Byte](12, 41, 51))
}
It's also possible to respond conditionally based on Content-Type
or Accept
header:
get("/api/thing") { request =>
respondTo(request) {
case _:Html => html("<p>html response</p>")
case _:Json => json(Map("value" -> "an json response"))
case _:All => "default fallback response"
}
}
Theres an embedded static file server which will serve out of src/main/resources/public
by default. It's also possible to render assets inside of routes:
get("/deal-with-it") { request =>
static("/dealwithit.gif")
}
It's important to note that the Router
runs before the file server, allowing you to dynamically intercept static assets:
get("/file.txt") { request =>
"this is the file"
}
To read headers, use request.headerMap
; much like request.params
, this is also a Map
get("/") { request =>
val isFoo = request.headerMap.getOrElse("X-Foo", "1")
"X-Foo status: " + isFoo
}
Setting headers is available on the Response
builder:
get("/") { request =>
plain("hi").header("Foo", "Bar")
}
You can call header
multiple times or pass a map to headers
:
get("/") { request =>
plain("hi")
.header("Foo", "Bar")
.header("Biz", "Baz")
}
get("/") { request =>
plain("hi")
.headers(Map("Foo" -> "Bar", "Biz" -> "Baz"))
}
Cookies, like Headers
, are read from request
and set via render
:
get("/") { request =>
val loggedIn = request.cookie("loggedIn").getOrElse("false")
"logged in?:" + loggedIn
}
get("/") { request =>
plain("hi")
.cookie("loggedIn", "true")
}
Advanced cookies are supported by creating and configuring Cookie
objects:
get("/") { request =>
val c = DefaultCookie("Biz", "Baz")
c.setSecure(true)
plain("get:path")
.cookie(c)
}
See the Cookie
class for more details.
Uploads are fully supported in the request.multiParams
object.
post("/profile") { request =>
request.multiParams.get("avatar").map { avatar =>
println("content type is " + avatar.contentType)
avatar.writeToFile("/tmp/avatar")
}
"ok"
}
See the MultipartItem
class for more details.
Filters are code that runs before any request is dispatched to a particular Controller
. They can modify the incoming request as well as the outbound response. A great example is our own LogginFilter
:
import com.twitter.finagle.{Service, SimpleFilter}
import com.twitter.util.Future
import com.twitter.finagle.http.{Request => FinagleRequest
import com.twitter.finagle.http.{Response => FinagleResponse}
import com.twitter.app.App
class LoggingFilter
extends SimpleFilter[FinagleRequest, FinagleResponse] with App with Logging {
def apply(
request: FinagleRequest,
service: Service[FinagleRequest, FinagleResponse])
) = {
val start = System.currentTimeMillis()
service(request) map { response =>
val end = System.currentTimeMillis()
val duration = end - start
log.info("%s %s %d %dms".format(request.method,
request.uri,
response.statusCode,
duration))
response
}
}
}
You can register these inside Peregrine
like so:
class MyServer extends PeregrineApp
addFilter(new SimpleFilter)
register(new ExampleController)
end
There is a log
log object available inside every Controller
with the standard error levels (info, warn, error, etc):
post("/profile") { request =>
try {
fetchProfileFromJankyServer()
} catch {
case exception => log.error(exception, "something bad happened")
}
log.info("sending ok")
"ok"
}
Theres also a default [StatsReceiver](https://github.com/twitter/finagle/blob/master/finagle-core/src/main/scala/com/twitter/finagle/stats/StatsReceiver.scala)
object available for recording metrics named stats
:
post("/profile") { request =>
try {
stats.counter("profile/attempts").incr
stats.time("profile/fetch") {
fetchProfileFromJankyServer()
}
} catch {
stats.counter("profile/fails").incr
case exception => log.error(exception, "something bad happened")
}
log.info("sending ok")
"ok"
}
These can be collected by visiting /admin/metrics.json
on the admin port, which is :9990
by default.
See the HTTP Admin Interface page of Twitter Server for more details.
You can unit test your controllers using the MockApp helper:
class SampleController extends Controller {
get("/testing") { request =>
"hi"
}
}
"GET /testing" should "be 200" in {
val app = MockApp(new SampleController)
val response = app.get("/testing")
response.code should be(200)
response.body should be("hi")
}
Alternatively, you can use SpecHelper trait to test the complete App:
class AppSpec extends FlatSpecHelper {
val app = new App.ExampleApp
"GET /" should "respond 200 with hi" in {
get("/")
response.body should equal ("hi")
response.code should equal (200)
}
}
To generate a deployable single jar "fatjar", you can use sbt-assembly
add the following to project/plugings.sbt
(if the folder doesn't exists, go ahead an create it)
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")
and then
sbt assembly
This produces a runnable jar with scala, peregrine, and any other dependent libraries included inside the target/
directory.
Note: If mustache views are being used, it's required to specify the resource directory in order to include them in the fatjar
resourceDirectory in Compile := baseDirectory.value / "app"
If you are using Heroku, create a Procfile like
web: java -Dio.peregrine.config.env=production -Dio.peregrine.config.adminPort='' -Dio.peregrine.config.port=:$PORT -cp target/classes:target/dependency/* app
and it will work out of the box.
peregrine flags for configuring the app arguments
java -jar myApp-0.1.1-SNAPSHOT.jar -io.peregrine.config.port=':4000'
Here's a full list of peregrine's flags available: (try -help to see it)
-io.peregrine.config.assetPath = '/public': path to assets
-io.peregrine.config.assetsPathPrefix = '/assets/': the prefix used to prefix assets url
-io.peregrine.config.certificatePath = '': path to SSL certificate
-io.peregrine.config.debugAssets = 'false': enable to show assets requests in logs
-io.peregrine.config.docRoot = 'src/main/resources': path to docroot
-io.peregrine.config.keyPath = '': path to SSL key
-io.peregrine.config.logLevel = 'INFO': log level
-io.peregrine.config.logNode = 'peregrine': Logging node
-io.peregrine.config.logPath = 'logs/peregrine.log': path to log
-io.peregrine.config.maxRequestSize = '5': maximum request size (in megabytes)
-io.peregrine.config.pidEnabled = 'false': whether to write pid file
-io.peregrine.config.pidPath = '': path to pid file
-io.peregrine.config.port = ':5000': Http Port
-io.peregrine.config.showDirectories = 'false': allow directory view in asset path
-io.peregrine.config.sslPort = ':5043': Https Port
-io.peregrine.config.templatePath = '/views': path to templates