WARNING: This page is a guide for 1.x series. See here instead to learn about the latest

Controller & Routes


Skinny’s routing mechanism and controller layer on MVC architecture can be thought of as a rich Scalatra.

Skinny’s extensions provide you with much simpler and richer syntax. Of course, if you need to use Scalatra’s API directly, Skinny never bothers you.

Scalatra

SkinnyController is a trait which extends ScalatraFilter and includes various useful components out-of-the-box.

// src/main/scala/controller/MembersController.scala
class MembersController extends SkinnyController {
  protectFromForgery() // CSRF protection enabled

  def index = {
    // set 'members' in the request scope, then you can use it in views
    set("members" -> Member.findAll())
    render("/members/index")
  }

  def newOne = render("/members/new")

  def createForm = validation(params,
    paramKey("name") is required & minLength(2),
    paramKey("countryId") is numeric
  )

  def createFormParams = params.permit(
    "name" -> ParamType.String, 
    "countryId" -> ParamType.Long)

  def create = if (createForm.validate()) {
    Member.createWithPermittedAttributes(createFormParams)
    redirect("/members")
  } else {
    render("/members/new")
  }

  // searches both query params and path params
  def show = params.getAs[Long]("id").map { id =>
    Member.findById(id).map { member =>
      set("member" -> member)
    }.getOrElse haltWithBody(404)
  }.getOrElse haltWithBody(404)

}

// src/main/scala/controller/Controllers.scala
object Controllers {
  object members extends MembersController with Routes {
    val indexUrl = get("/members/?")(index).as('index)
    val newUrl = get("/members/new")(newOne).as('new)
    val createUrl = post("/members/?")(create).as('create)
    val showUrl = get("/members/:id")(show).as('show)
  }
}

// src/main/scala/ScalatraBootstrap.scala
class ScalatraBootstrap extends SkinnyLifeCycle {
  override def initSkinnyApp(ctx: ServletContext) {
    // register routes
    Controllers.members.mount(ctx)
  }
}

#render expects src/main/webapp/WEB-INF/views/members/index.html.ssp by default.

render("/members/index")
<!-- src/main/webapp/WEB-INF/views/members/index.html.ssp -->
<%@val members: Seq[Member]%>
<ul>
#for (member <- members)
  <li>${member.name}</li>
#end
<ul>

Reverse Routes


You can use Scalatra’s reverse routes.

http://www.scalatra.org/2.3/guides/http/reverse-routes.html

In controllers:

class MembersController extends SkinnyController {
  def oldPage = redirect(url(Controllers.members.indexUrl))
}

object Controllers {
  object members extends MembersController with Routes {
    val indexUrl = get("/members/?")(index).as('index)
    val showUrl = get("/members/:id")(show).as('show)
  }
}

In views:

// Jade example
a(href={s.url(Controllers.members.showUrl, "id" -> member.id)}) Show detail

FYI: You can see more examples for SkinnyResource by generating scaffold views.


SkinnyController & SkinnyServlet


There are the two controller base traits. SkinnyController is a ScalatraFilter. SkinnyServlet is a ScalatraServlet.

In general SkinnyController is more suitable for Skinny framework based applications.

However, if you really need to use a ScalatraServlet instead of a filter, use SkinnyServlet.


SkinnyResource


SkinnyResource is a useful base trait for RESTful web services. SkinnyResource is very similar to Rails ActiveResource.

Resource Routing: the Rails Default (Rails Routing from the Outside In — Ruby on Rails Guides)

https://github.com/rails/activeresource

SkinnyResource is also useful as a controller sample. If you’re a Skinny beginner, take a look at its code.

framework/src/main/scala/skinny/controller/SkinnyResource.scala

SkinnyResourceActions has action methods for the resource and SkinnyResourceRoutes defines routings for the resource. If you’d like to customize routings (e.g. use only creation and deletion), just mixin only SkinnyResourceActions and define routings by yourself.

You can see a SkinnyResource example using the scaffolding generator.

./skinny g scaffold members member name:String birthday:LocalDate

Then you get the following code. If you need to customize, override some parts of SkinnyResource:

framework/src/main/scala/skinny/controller/SkinnyResource.scala

package controller

import skinny._
import skinny.validator._
import model.Member

object MembersController extends SkinnyResource {
  protectFromForgery() // CSRF protection

  override def model = Member // SkinnyModel for this controller
  override def resourcesName = "members"
  override def resourceName = "member"

  // parameters & validations for creation
  override def createParams = Params(params).withDate("birthday")
  override def createForm = validation(createParams,
    paramKey("name") is required & maxLength(512),
    paramKey("birthday") is required & dateFormat
  )
  override def createFormStrongParameters = Seq(
    "name" -> ParamType.String,
    "birthday" -> ParamType.LocalDate
  )

  // parameters & validations for modification
  override def updateParams = Params(params).withDate("birthday")
  override def updateForm = validation(updateParams,
    paramKey("name") is required & maxLength(512),
    paramKey("birthday") is required & dateFormat
  )
  override def updateFormStrongParameters = Seq(
    "name" -> ParamType.String,
    "birthday" -> ParamType.LocalDate
  )

}

Required view templates:

src/main/webapp/WEB-INF/views/members/
├── _form.html.ssp
├── edit.html.ssp
├── index.html.ssp
├── new.html.ssp
└── show.html.ssp

In this case, SkinnyResource provides the following URLs from SkinnyResourceRoutes by default. If you don’t need all the routes below, just mixin SkinnyResourceActions instead and specify only routes you need by hand.


skinny.Skinny


skinny.Skinny provides getters for basic elements in view templates.

<%@val s: skinny.Skinny %>

framework/src/main/scala/skinny/Skinny.scala


beforeAction/afterAction Filters


Scalatra has before/after filters by default, but we recommend Skinny users to use Skinny’s beforeAction/afterAction filters.

The reason is that Scalatra’s filters might affect other controllers that are defined further down in ScalatraBootstrap.scala and it’s not easy to figure out where each controller’s before/after affects completely.

So if you need filters that are similar to Rails filters, just use Skinny filters.

Scalatra’s before/after filters

/api/index.html#org.scalatra.ScalatraBase

Skinny’s beforeAction/afterAction filters

/framework/src/main/scala/skinny/controller/feature/BeforeAfterActionFeature.scala

class MembersController extends SkinnyController with Routes {

  // Scalatra filters
  before() { ... } // might affect other controllers
  after() { ... }

  // Skinny filters
  beforeAction(only = Seq('index, 'new)) {
    println("before")
  }
  afterAction(except = Seq('new)) {
    println("after")
  }

  //actions
  def index = ...
  def newInput = ...
  def edit = ...

  // routes
  get("/members/?")(index).as('index)      // before, after
  get("/members/new")(newInput).as('new)   // before
  get("/members/:id/edit")(edit).as('edit) // after

}

SkinnyFilter


SkinnyFilter is our original filter to enable easily handling before/after/error for each controller. You can apply the same beforeAction/afterAction/error filters to several controllers by just mixing in SkinnyFilter-based traits.

Contrary to your expectations, Scalatra doesn’t run all the handlers for after and error. Only the first one would be applied and the others are just ignored. We think it’s difficult to be aware of this specification (it’s not a bug but by design). So SkinnyFilter’s before/after/error handlers are always applied.

For instance, in skinny-blank-app, ErrorPageFilter is applied to ApplicationController.

trait ApplicationController extends SkinnyController
  with ErrorPageFilter {

}

ErrorPageFilter is as follows. It’s pretty easy to customize such a filter (e.g. adding error notification) as follows.

trait ErrorPageFilter extends SkinnyRenderingFilter {
  // appends error filter in order
  addRenderingErrorFilter {
    case e: Throwable =>
      logger.error(e.getMessage, e)
      try {
        status = 500
        render("/error/500")
      } catch {
        case e: Exception => throw e
      }
  }
}

It just outputs error logging, set status as 500 and renders “/error/500.html.ssp” or similar templates. SkinnyRenderingFilter is a sub-trait of SkinnyFilter. SkinnyFilter’s response value is always ignored. But SkinnyRenderingFilter can return a response body like above.

The second example is TxPerRequestFilter which enables you to apply the open-session-in-view pattern to your apps.

trait TxPerRequestFilter extends SkinnyFilter with Logging {
  def cp: ConnectionPool = ConnectionPool.get()

  def begin = {
    val db = ThreadLocalDB.create(cp.borrow())
    db.begin()
  }

  // will handle exceptions occurred in controllers
  addErrorFilter { case e: Throwable =>
    Option(ThreadLocalDB.load()).foreach { db =>
      if (db != null && !db.isTxNotActive) using(db)(_.rollbackIfActive())
    }
  }

  def commit = {
    val db = ThreadLocalDB.load()
    if (db != null && !db.isTxNotActive) {
      using(db)(_.commit())
    }
  }

  beforeAction()(begin)
  afterAction()(commit)
}

How to Do It? Examples


Basically, you will use Scalatra’s DSL.

http://www.scalatra.org/2.3/guides/

http://www.scalatra.org/2.3/api/index.html#org.scalatra.ScalatraBase


(Query/Form/Path) Parameters


params

val name: Option[String] = params.get("id")
val id: Option[Int] = params.getAs[Int]("id")
val id: Long = params.getAsOrElse[Long]("id", -1L)

val ids: Option[Seq[Int]] = multiParams.getAs[Int]("ids")

// additional APIs are provided from Skinny 
queryParams: Params
queryMultiParams: MultiParams
formParams: Params
formMultiParams: MultiParams

/api/index.html#org.scalatra.ScalatraParams

/api/index.html#org.scalatra.ScalatraParamsImplicits$TypedParams

/api/index.html#org.scalatra.ScalatraParamsImplicits$TypedMultiParams

framework/src/main/scala/skinny/controller/feature/QueryParamsFeature.scala

framework/src/main/scala/skinny/controller/feature/FormParamsFeature.scala


multiParams(“splat”)

get("/say/*/to/*") {
  // Matches "GET /say/hello/to/world"
  multiParams("splat") // == Seq("hello", "world")
}

get("/download/*.*") {
  // Matches "GET /download/path/to/file.xml"
  multiParams("splat") // == Seq("path/to/file", "xml")
}

/api/index.html#org.scalatra.ScalatraBase

/guides/http/routes.html


multiParams(“captures”)

get("""^\/f(.*)/b(.*)""".r) {
  // Matches "GET /foo/bar"
  multiParams("captures") // == Seq("oo", "ar")
}

/api/index.html#org.scalatra.ScalatraBase

/guides/http/routes.html


Cookies


def hello = {
  cookies += "name" -> "value"
  cookies -= "name"
}

def helloWithOptions = {
  implicit val options = CookieOptions(secure = true)
  cookies.set("name" -> "value")
  cookies.set("name" -> "value")(options)
}

/api/index.html#org.scalatra.SweetCookies

/api/org/scalatra/CookieOptions.html


Request/Response Headers


val v: Option[String] = request.header(name)

/api/index.html#org.scalatra.servlet.RichRequest

response.headers += "name" -> "value"
response.headers -= "name"

/api/index.html#org.scalatra.servlet.RichResponse


Session


val v: Any = session("name") // or session('name)
session += "name" -> "value"
session -= "name"

/api/index.html#org.scalatra.servlet.RichSession

Even if you don’t use sessions in your application code, Scalatra’s Flash and CSRF protection features are using servlet sessions. So your apps are not naturally stateless when using vanilla Scalatra.

We recommend you use SkinnySession to keep your Skinny apps stateless. When you enable SkinnySession, these features also start using SkinnySession instead of Servlet session attributes.

ScalatraBootstrap.scala:

import scalikejdbc._
import skinny.session.SkinnySessionInitializer
class ScalatraBootstrap extends SkinnyLifeCycle {
  override def initSkinnyApp(ctx: ServletContext) {
    // Database queries will be increased
    GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(
      singleLineMode = true
    )
    ctx.mount(classOf[SkinnySessionInitializer], "/*")

    Controllers.root.mount(ctx)
    AssetsController.mount(ctx)
  }
}

controller/RootController.scala:

class RootController extends ApplicationController with SkinnySessionFilter {

  def index = {
    val v: Option[Any] = skinnySession.getAttribute("name")
    skinnySession.setAttribute("name", "value")
    skinnySession.removeAttribute("name")
  }
}

DB migration file:

-- H2 Database compatible
create table skinny_sessions (
  id bigserial not null primary key,
  created_at timestamp not null,
  expire_at timestamp not null
);
create table servlet_sessions (
  jsession_id varchar(32) not null primary key,
  skinny_session_id bigint not null,
  created_at timestamp not null,
  foreign key(skinny_session_id) references skinny_sessions(id)
);
create table skinny_session_attributes (
  skinny_session_id bigint not null,
  attribute_name varchar(128) not null,
  attribute_value bytea,
  foreign key(skinny_session_id) references skinny_sessions(id)
);
alter table skinny_session_attributes add constraint
  skinny_session_attributes_unique_idx
  unique(skinny_session_id, attribute_name);

Flash


Notice: Flash uses servlet sessions by default. Be aware of sticky session mode.

flash(name) = value
flash += (name -> value)
flash.now += (name -> value)

/guides/http/flash.html

/api/index.html#org.scalatra.FlashMap


Request Body


val body: String = request.body
val stream: InputStream = request.inputStream // raw HTTP POST data

/api/index.html#org.scalatra.servlet.RichRequest


File Upload


WARNING: Extend not SkinnyController but SkinnyServlet. You cannot use FileUploadFeature with SkinnyController.
import skinny.SkinnyServlet
import skinny.controller.feature.FileUploadFeature

// SkinnyServlet!!!
class FilesController extends SkinnyServlet with FileUploadFeature {

  def upload = fileParams.get("file") match {
    case Some(file) =>
      Ok(file.get(), Map(
        "Content-Type"        -> (file.contentType.getOrElse("application/octet-stream")),
        "Content-Disposition" -> ("attachment; filename=\"" + file.name + "\"")
      ))
    case None =>
      BadRequest(displayPage(
        <p>
          Hey! You forgot to select a file.
        </p>))
  }
}

/guides/formats/upload.html

/api/index.html#org.scalatra.fileupload.FileUploadSupport


Response handling


halt(404)
halt(status = 400, headers = Map("foo" -> "bar"), reason = "why")

redirect("/top") // 302
redirect301("/new_url") // 301
redirect302("/somewhere") // 302
redirect303("/complete") // 303

/api/index.html#org.scalatra.ScalatraBase

/index.html#org.scalatra.ActionResult

/framework/src/main/scala/skinny/controller/feature/ExplicitRedirectFeature.scala

/core/src/main/scala/org/scalatra/ActionResult.scala


CSRF Protection


Notice: Scalatra CSRF protection implementation uses servlet sessions by default. Be aware of sticky session mode.

Define protectFromForgery in controller/RootController.scala:

class RootController extends ApplicationController {
  protectFromForgery()

}

Use Skinny.csrfMetaTags in /WEB-INF/layouts/default.jade:

-@val body: String
-@val s: skinny.Skinny
!!! 5
%html(lang="en")
  %head
    %meta(charset="utf-8")
    != s.csrfMetaTags
  %body
    =unescape(body)
    %script(type="text/javascript" src={uri("/assets/js/skinny.js")})

Check execution time


val result = warnElapsedTime(1) {
  Thread.sleep(10)
}
// will output "[SLOW EXECUTION DETECTED] Elapsed time: 10 millis"

In controllers, you can add request info:

val result = warnElapsedTimeWithRequest(1) {
  Thread.sleep(10)
}

Read your own configuration values


development {
  defaultLabel="foo"
  timeout {
    connect=1000
    read=3000
  }
  memcached=["server1:11211", "server2:11211"]
}

Read the server names like this:

val label: Option[String] = SkinnyConfig.stringConfigValue("defaultLabel")
val connectTimeout: Option[Int] = SkinnyConfig.intConfigValue("timeout.connect")
val memcachedServers: Option[Seq[String]] = SkinnyConfig.stringSeqConfigValue("memcached")

Awaiting several Futures within action method


The following is a simple dashboard application which collects several futures within the index method.

Since Skinny 1.2.0, you can easily access request and requestScope from Futures by using futureWithRequest block.

package controller

import _root_.service._
import javax.servlet.http.HttpServletRequest
import org.joda.time._
import scala.concurrent._
import scala.concurrent.duration._

// using your own ExecutionContext will be preferred in most cases
import scala.concurrent.ExecutionContext.Implicits.global

case class DashboardOps(controller: DashboardController) {
  def setCurrentUser(implicit req: HttpServletRequest) = {
    // implicit request is not ambiguous here
    val userId = controller.currentUserId.getOrElse(controller.halt(401))
    controller.set("currentUser" -> controller.adminUserService.getCurrentUser(userId))
  }
}

class DashboardController extends ApplicationController {

  val adminUserService = new AdminUserService
  val accessService = new AccessLogService
  val alertService = new AlertService
  val ops = DashboardOps(this)

  def index = {
    val scope: scala.collection.concurrent.Map[String, Any] = requestScope()

    awaitFutures(5.seconds)(
      // simply define operation inside of this controller
      futureWithRequest { implicit req =>
        //set("hourlyStats", accessService.getHourlyStatsForGraph(new LocalDate))
        set("hourlyStats", accessService.getHourlyStatsForGraph(new LocalDate))(req)

// [error] example/src/main/scala/controller/DashboardController.scala:43: ambiguous implicit values:
// [error]  both value req of type javax.servlet.http.HttpServletRequest
// [error]  and method request in class DashboardController of type => javax.servlet.http.HttpServletRequest
// [error]  match expected type javax.servlet.http.HttpServletRequest
// [error]         set("hourlyStats", accessService.getHourlyStatsForGraph(new LocalDate))
// [error]            ^
// [error] one error found
      },

      // separate operation to outside of this controller
      futureWithRequest(req => ops.setCurrentUser(req)),

      // just use Future directly
      Future {
        // When using Future directly, you must be aware of Scalatra's DynamicScope's thread local request.
        // In this case, you cannot use request here. requestScope, session and so on 
        // should be captured outside of this Future block.
        scope.update("alerts", alertService.findAll())
      }
    )
    render("/dashboard/index")
  }

  private[controller] def currentUserId(implicit req: HttpServletRequest): Option[Long] = session(req).getAs[Long]("userId")
}
If you find a typo or mistake in this page, please report or fix it. How?