Resty
Super easy REST API framework for Scala
You can run the sample project by hitting following commands:
$ git clone https://github.com/takezoe/resty-sample.git
$ cd resty-sample/
$ sbt ~jetty:start
Check APIs via Swagger UI at: http://localhost:8080/swagger-ui/
.
Getting started
This is a simplest controller example:
import com.github.takezoe.resty._
class HelloController {
@Action(method = "GET", path = "/hello/{name}")
def hello(name: String): Message = {
Message(s"Hello ${name}!")
}
}
case class Message(message: String)
Define a web listener that registers your controller.
@WebListener
class InitializeListener extends ServletContextListener {
override def contextDestroyed(sce: ServletContextEvent): Unit = {
}
override def contextInitialized(sce: ServletContextEvent): Unit = {
Resty.register(new HelloController())
}
}
Let's test this controller.
$ curl -XGET http://localhost:8080/hello/resty
{"message": "Hello resty!" }
Annotations
Resty provides some annotations including @Action
.
@Controller
You can add @Controller
to the controller class to define the controller name and description. They are applied to Swagger JSON.
parameter | required | description |
---|---|---|
name | optional | name of the controller |
description | optional | description of the controller |
@Controller(name = "hello", description = "HelloWorld API")
class HelloController {
...
}
@Action
We already looked @Action
to annotate the action method. It has some more parameters to add more information about the action.
parameter | required | description |
---|---|---|
method | required | GET, POST, PUT or DELETE |
path | required | path of the action ({name} defines path parameter) |
description | optional | description of the method |
deprecated | optional | if true then deprecated (default is false) |
class HelloController {
@Action(method = "GET", path = "/v1/hello",
description = "Old version of HelloWorld API", deprecated = true)
def hello() = {
...
}
}
@Param
@Param
is added to the arguments of the action method to define advanced parameter binding.
parameter | required | description |
---|---|---|
from | optional | query, path, header or body |
name | optional | parameter or header name (default is arg name) |
description | optional | description of the parameter |
class HelloController {
@Action(method = "GET", path = "/hello")
def hello(
@Param(from = "query", name = "user-name") userName: String,
@Param(from = "header", name = "User-Agent") userAgent: String
) = {
...
}
}
Types
Resty supports following types as the parameter argument:
Unit
String
Int
Long
Boolean
Option[T]
Seq[T]
Array[T]
Array[Byte]
(for Base64 encoded string)AnyRef
(for JSON in the request body)
Also following types are supported as the return value of the action method:
String
is responded astext/plain; charset=UTF-8
Array[Byte]
,InputStream
,java.io.File
are responded asapplication/octet-stream
AnyRef
is responded asapplication/json
ActionResult[_]
is responded as specified status, headers and bodyFuture[_]
is processed asynchronously usingAsyncContext
Servlet API
You can access Servlet API by defining method arguments with following types:
HttpServletRequest
HttpServletResponse
HttpSession
ServletContext
class HelloController {
@Action(method = "GET", path = "/hello")
def hello(request: HttpServletRequest): Message = {
val name = request.getParameter("name")
Message(s"Hello ${name}!")
}
}
Validation
It's possible to validate JSON properties by asserting properties in the constructor of the mapped case class.
case class Message(message: String){
assert(message.length < 10, "message must be less than 10 charactors.")
}
When the parameter value is invalid, Resty responds the following response with the 400 BadRequest
status:
{
"errors": [
"message must be less than 10 charactors."
]
}
HTTP client
HttpClientSupport
trait offers methods to send HTTP request. You can call other Web APIs easily using these methods.
class HelloController extends HttpClientSupport {
@Action(method = "GET", path = "/hello/{id}")
def hello(id: Int): Message = {
// Call other API using methods provided by HttpClientSupport
val user: User = httpGet[User](s"http://localhost:8080/user/${id}")
Message(s"Hello ${user.name}!")
}
@Action(method = "GET", path = "/hello-async/{id}")
def helloAsync(id: Int): Future[Message] = {
// HttpClientSupport also supports asynchronous communication
val future: Future[Either[ErrorModel, User]] = httpGetAsync[User](s"http://localhost:8080/user/${id}")
future.map {
case Right(user) => Message(s"Hello ${user.name}!")
case Left(error) => throw new ActionResultException(InternalServerError(error))
}
}
}
These methods have retrying ability and circuit breaker. You can configure these behavior by defining HttpClientConfig
as an implicit value.
class HelloController extends HttpClientSupport {
implicit override val httpClientConfig = HttpClientConfig(
maxRetry = 5, // max number of retry. default is 0 (no retry)
retryInterval = 500, // interval of retry (msec). default is 0 (retry immediately)
maxFailure = 3, // max number until open circuit breaker. default is 0 (disabling circuit breaker)
resetInterval = 60000 // interval to reset closed circuit breaker (msec). default is 60000
)
...
}
Swagger integration
Resty provides Swagger integration in default. Swagger JSON is provided at http://localhost:8080/swagger.json
and also Swagger UI is available at http://localhost:8080/swagger-ui/
.
Add following parameter to web.xml
to enable Swagger integration:
<context-param>
<param-name>resty.swagger</param-name>
<param-value>enable</param-value>
</context-param>
By enabling runtime-scaladoc-reader plugin in your project, Scaladoc of controller classes is reflected to Swagger JSON. For example, Scaladoc of the controller class is used as the tag description, and Scaladoc of the method is used as the operation description, the parameter description and the response description.
addCompilerPlugin("com.github.takezoe" %% "runtime-scaladoc-reader" % "1.0.1")
Hystrix integration
Resty also provides Hystrix integration in default. Metrics are published for each operations. The stream endpoint is available at http://localhost:8080/hystrix.stream
. Register this endpoint to the Hystrix dashboard.
Add following parameter to web.xml
to enable Hystrix integration:
<context-param>
<param-name>resty.hystrix</param-name>
<param-value>enable</param-value>
</context-param>
Zipkin integration
Furthermore, Resty supports Zipkin as well. You can send execution results to the Zipkin server by enabling Zipkin support and using HttpClientSupport
for calling other APIs.
Add following parameters to web.xml
to enable Zipkin integration:
<context-param>
<param-name>resty.zipkin</param-name>
<param-value>enable</param-value>
</context-param>
<context-param>
<param-name>resty.zipkin.service.name</param-name>
<param-value>resty-sample</param-value>
</context-param>
<context-param>
<param-name>resty.zipkin.sample.rate</param-name>
<param-value>1.0</param-value>
</context-param>
<context-param>
<param-name>resty.zipkin.server.url</param-name>
<param-value>http://127.0.0.1:9411/api/v1/spans</param-value>
</context-param>
WebJars support
WebJars is a cool stuff to integrate frontend libraries with JVM based applications. Resty can host static files that provided by WebJars for frontend applications.
Add a following parameter to web.xml
to enable WebJars hosting:
<context-param>
<param-name>resty.wabjars</param-name>
<param-value>enable</param-value>
</context-param>
<context-param>
<param-name>resty.wabjars.path</param-name>
<param-value>/public/assets/*</param-value>
</context-param>
You can add WebJars dependencies in your application as following:
libraryDependencies += "org.webjars" % "jquery" % "3.1.1-1"
Then import JavaScript library as following:
<script src="/public/assets/jquery.min.js" type='text/javascript'></script>
Static files hosting
Resty is including some base servlets to host static files. You can provide a frontend application through Resty application from the classpath or the file system by defining following servlet based on these classes.
// Host static files on the file system
@WebServlet(name="FileResourceServlet", urlPatterns=Array("/public/*"))
class MyFileResourceServlet extends FileResourceServlet("src/main/webapp")
// Host static files in the classpath
@WebServlet(name="ClasspathResourceServlet", urlPatterns=Array("/public/*"))
class MyClasspathResourceServlet extends ResourceServlet("com/github/resty/sample/public")
CORS support
CORS support can be enabled by adding following parameters to web.xml
:
<context-param>
<param-name>resty.cors</param-name>
<param-value>enable</param-value>
</context-param>
<context-param>
<param-name>resty.cors.allowedOrigins</param-name>
<param-value>http://localhost:8080</param-value>
</context-param>
<context-param>
<param-name>resty.cors.allowedMethods</param-name>
<param-value>GET, POST</param-value>
</context-param>
<context-param>
<param-name>resty.cors.allowedHeaders</param-name>
<param-value>Content-Type</param-value>
</context-param>
<context-param>
<param-name>resty.cors.allowCredentials</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>resty.cors.preflightMaxAge</param-name>
<param-value>1800</param-value>
</context-param>
Description about optional parameters:
resty.cors.allowedOrigins
: Comma separated list of hosts and ports which will be allowed to make cross-origin requests (default is*
).resty.cors.allowedMethods
: Comma separated list of HTTP methods will be allowed (default isGET, POST, PUT, DELETE
).resty.cors.allowedHeaders
: Comma separated list of allowed HTTP headers (most headers are allowed in default).resty.cors.allowCredentials
: Set this parameter to true to allow cookies in CORS requests (default isfalse
).resty.cors.preflightMaxAge
: Number of seconds that preflight request can be cached in the client (default is0
).