Skip to content

Commit

Permalink
First release
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianhurt committed Sep 13, 2015
0 parents commit 76c5d77
Show file tree
Hide file tree
Showing 38 changed files with 2,714 additions and 0 deletions.
87 changes: 87 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Ignore all dotfiles...
.*
# except for .gitignore
!.gitignore

# Ignore Play! working directory #
######################
bin
db
eclipse
.eclipse
lib
log
logs
precompiled
project/project
project/target
target
.target
tmp
test-result
server.pid
*.iml
*.eml
dist
.cache
public/clients
.history
/out
RUNNING_PID
activator*
# except for activator.properties
!activator.properties

# Ignore Play Modules within Modules #
modules/*/modules



# Compiled source #
###################
*.com
*.class
*.dll
*.exe
*.o
*.so

# Packages #
############
# it's better to unpack these files and commit the raw source
# git has its own built in compression methods
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip

# Logs and databases #
######################
*.log
*.sql
*.sqlite

# OS generated files #
######################
.DS_Store*
ehthumbs.db
Icon?
Thumbs.db


# general
*~

# Eclipse
.settings
.project
.classpath

# IDEA
/.idea
/.idea_modules

8 changes: 8 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
This software is licensed under the Apache 2 license, quoted below.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with
the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.
249 changes: 249 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# Play API REST Template [Play 2.4 - Scala]

Template to show how to implement an API using Play Framework.

__There is another companion project to test this template [here](https://github.com/adrianhurt/play-api-rest-tester).__

__Note:__ All this information is also available as a tutorial if you run the app using [Activator UI](http://typesafe.com/platform/getstarted).

The characteristics and objectives of this template are:

* RESTful.
* JSON: for request and response bodies.
* Assumes SSL everywhere (SSL is not covered in this template).
* Use of Authentication Token to sign in.
* As much standard as possible.
* Allow pagination.
* Allow filtering, sorting and searching.
* Priorize the simplicity for creating new actions and controllers.
* Allow enveloping for those API clients those haven't access to HTTP Headers.

What this template does NOT explain:

* SSL: you can configure Play to do that. Or better, configure your web server.
* Security Issues like password storing.
* ETag, Last-Modified.
* GZIP.
* DB storage.

Please, __read this__ interesting post:
[Best Practices for Designing a Pragmatic RESTful API](http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api)
(also [in Spanish](http://elbauldelprogramador.com/buenas-practicas-para-el-diseno-de-una-api-restful-pragmatica/)).

First of all, this is a template, not a library. It simply tries to give you some clues to develop your own API. So copy the files you need, adapt them to your code and play with it.

And please, don't forget starring this project if you consider it has been useful for you.

Also check my other projects:

* [Play Multidomain Seed [Play 2.4 - Scala]](https://github.com/adrianhurt/play-multidomain-seed)
* [Play-Bootstrap3 - Play library for Bootstrap 3 [Scala & Java]](http://play-bootstrap3.herokuapp.com)

## Basic structure

It's simple. In Play you have the classes `Controller`, `Action[A]`, `Request[A]` and `Result`. With this template we have their equivalences:

* `Controller` -> `ApiController`
* `Action[A]` -> `GetAction`, `PostAction`, `SecuredGetAction`, `SecuredPostAction`, ... and more.
* `Request[A]` -> `ApiRequest[A]` and `SecuredApiRequest[A]`
* `Result` -> `ApiResponse` and `ApiError`, both extending the trait `ApiResult`.

### Api

Object that contains common values and methods.

### ApiRequest[A] and SecuredApiRequest[A]

Every request should have the following headers:

* __Content-Type__: application/json
* __Date__: with format _"E, dd MMM yyyy HH:mm:ss 'GMT'"_
* __X-Api-Key__: with the corresponding Api Key
* __X-Auth-Token__: with the corresponding Auth Token (only for __secured requests__)
* __Accept-Language__: with the desired language (optional)

`ApiRequest[A]` it's a simple `RequestWrapper[A]` with several additional values:

* `apiKeyOpt: Option[String]`: with the Api Key taken from the `X-Api-Key` header.
* `dateOpt: Option[DateTime]`: with the date taken from the header.
* `tokenOpt: Option[String]`: with the Auth Token taken from the `X-Auth-Token` header.

`SecuredApiRequest[A]` extends from `ApiRequest[A]` and it's used for secured requests where the user must be signed in.

* `apiKey: String`
* `date: DateTime`
* `token: String`
* `userId: Long`: with the id of the signed user.

### ApiResult

It's a trait that stores the information required to create a `Result`. It stores the Status Code, the JSON value and the headers for the response.

### ApiResponse

It extends from `ApiResult` and it represents a successful response for the request. The available Status Codes are: `STATUS_OK`, `STATUS_CREATED`, `STATUS_ACCEPTED` and `STATUS_NOCONTENT`. And it provides the following factory methods:

* `ok(json: JsValue, headers: (String, String)*)`
* `ok[A](json: JsValue, page: Page[A], headers: (String, String)*)`
* `created(json: JsValue, headers: (String, String)*)`
* `accepted(json: JsValue, headers: (String, String)*)`
* `noContent(headers: (String, String)*)`

### ApiError

It extends from `ApiResult` and it represents an error response for the request. It stores a specific error code, a descriptive message and an optional additional object with more information. The JSON structure is the like the following:

{ code: 400, msg: "Bad Request" }

or

{ code: 125, msg: "Malformed body", info: "additional information about the error" }

The available Status Codes are: `STATUS_BADREQUEST`, `STATUS_UNAUTHORIZED`, `STATUS_FORBIDDEN`, `STATUS_NOTFOUND` and `STATUS_INTERNAL_SERVER`. And it provides a list of predefined code errors and their corresponding factory methods.

### ApiController

It's simply a trait that extends from `Controller` and adds a set of utilities to do the life easier.

#### Actions

There are a list of useful actions for each request method:

* `GetAction(action: ApiRequest[Unit] => Future[ApiResult])(implicit m: Messages)`
* `PostAction(action: ApiRequest[JsValue] => Future[ApiResult])(implicit m: Messages)`
* `PutAction(action: ApiRequest[JsValue] => Future[ApiResult])(implicit m: Messages)`
* `PatchAction(action: ApiRequest[JsValue] => Future[ApiResult])(implicit m: Messages)`
* `DeleteAction(action: ApiRequest[Unit] => Future[ApiResult])(implicit m: Messages)`

And their equivalences for secured requests:

* `SecuredGetAction(action: SecuredApiRequest[Unit] => Future[ApiResult])(implicit m: Messages)`
* `SecuredPostAction(action: SecuredApiRequest[JsValue] => Future[ApiResult])(implicit m: Messages)`
* `SecuredPutAction(action: SecuredApiRequest[JsValue] => Future[ApiResult])(implicit m: Messages)`
* `SecuredPatchAction(action: SecuredApiRequest[JsValue] => Future[ApiResult])(implicit m: Messages)`
* `SecuredDeleteAction(action: SecuredApiRequest[Unit] => Future[ApiResult])(implicit m: Messages)`

#### Creating `ApiResults` from writable JSON objects

There are a set of useful methods to create `ApiResults` from JSON objects:

* `ok[A](obj: A, headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `ok[A](futObj: Future[A], headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `maybeItem[A](opt: Option[A], headers: (String, String)*)(implicit w: Writes[A], m: Messages): Future[ApiResult]`
* `maybeItem[A](futOpt: Future[Option[A]], headers: (String, String)*)(implicit w: Writes[A], m: Messages): Future[ApiResult]`
* `page[A](p: Page[A], headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `page[A](futP: Future[Page[A]], headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `created[A](obj: A, headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `created[A](futObj: Future[A], headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `accepted[A](obj: A, headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `accepted[A](futObj: Future[A], headers: (String, String)*)(implicit w: Writes[A]): Future[ApiResult]`
* `noContent(headers: (String, String)*): Future[ApiResult]`

## Pagination

There is a simple class to hold the pagination information:

case class Page[+A](items: Seq[A], page: Int, size: Int, total: Long) {
def offset = (page - 1) * size + 1
}

Then, we need to create a `Page` class with our list of items and simply call the `page()` method of`ApiController`.

val itemsPage: Future[Page[Item]] = ...
itemsPage.map(page(_))

or simply:

page(itemsPage)

It will add automatically the following headers to the response:

* `X-Page`: current page (int)
* `X-Page-From`: the position of the first item of this page within the total of items (int)
* `X-Page-Size`: number of items for each page (int)
* `X-Page-Total`: total number of items (int)

## Filtering, searching and sorting

Filtering and searching are very straightforward. Imagine you have an action to list the tasks in a TO-DO list. If you would like to add the ability to filter those that are done or not, or to seach for a specific query term, you only have to add this parameters and handle them to implement them within your search engine.

/tasks?q=blah&done=true
# Route
GET /tasks controllers.Tasks.list(q: Option[String], done: Option[Boolean])

However, sorting is trickier. Let's see this example:

/tasks?sort=-deadline,-date,order
# Route
GET /tasks controllers.Tasks.list(sort: Option[String])

The `sort` parameter have a comma separated list of signed fields, ordered by priority. So for the example, you would like to order by deadline in descendent order, then by date in descendent order, and finally by the task's order within the list in ascendent order. To make it a bit easier, `ApiController` defines the following method

processSortByParam(sortBy: Option[String], allowedFields: Seq[String], default: String): Either[ApiError, Seq[(String, Boolean)]]

It takes the `sort` parameter, a list of available allowed fields to sort by, and a default sorting string. Then it returns a list of pairs `(String, Boolean)` with the corresponding field and order (true if it's descendent), or an `ApiError` if any of the field is not allowed.

Then, you can implement the sorting as you want from this `Seq[(field, order)]`, depending on your DB storage.

## Envelope

There are cases where an API client can't access to the HTTP Headers. For that cases you can add the parameter `envelope=true` to the query and it will encapsulate the data information and the headers within a JSON object like that:

{ data: your-json-response, status: status-code, headers: { header1: value1, header2: value2, … } }

## How to use it: a simple TODO list example

This template has implemented a basic TODO example, so you can check it. But here let's see some examples.

Your controllers should extend `ApiController` and `I18nSupport`.

class Tasks @Inject() (val messagesApi: MessagesApi) extends api.ApiController with I18nSupport { … }

To list the tasks and allow searching, filtering, sorting and pagination:

def list(folderId: Long, q: Option[String], done: Option[Boolean], sort: Option[String], p: Int, s: Int) = SecuredGetAction { implicit request =>
sortedPage(sort, Task.sortingFields, default = "order") { sortingFields =>
Task.page(folderId, q, done, sortingFields, p, s)
}
}

Where `Task.page(…)` implements the functionality to return a `Page[Task]` with the corresponding tasks applying this parameters.

To insert new items, there is another method in `ApiController` that reads a writable object and returns an `ApiError` if needed.

def insert(folderId: Long) = SecuredPostAction { implicit request =>
readFromRequest[Task] { task =>
Task.insert(folderId, task.text, new Date(), task.deadline).flatMap {
case (id, newTask) =>
ok(newTask)
}
}
}

To return a single item, update it and delete it:

def info(id: Long) = SecuredGetAction { implicit request =>
maybeItem(Task.findById(id))
}
def update(id: Long) = SecuredPutAction { implicit request =>
readFromRequest[Task] { task =>
Task.basicUpdate(id, task.text, task.deadline).flatMap { isOk =>
if (isOk) noContent() else errorInternal
}
}
}
def delete(id: Long) = SecuredDeleteAction { implicit request =>
Task.delete(id).flatMap { _ =>
noContent()
}
}

The authorization based on the Auth Token is done within the `Auth` controller. It handles the sign in and sign out actions to handle the tokens, and sign up one to register a new user.

### FakeDB

For the example, a fake DB is implemented. The implementation is not relevant, but you can check all the information within the DB in your browser at `/fakeDB`. That will let you check what's happening while you're testing with the [tester](https://github.com/adrianhurt/play-api-rest-tester).

## Test Units

Although there is a companion [tester project](https://github.com/adrianhurt/play-api-rest-tester), don't forget to use unit testing for production. You can see some examples in this template.
4 changes: 4 additions & 0 deletions activator.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name=play-api-rest-seed
title=Play 2.4 API REST Seed
description=This Play 2.4 template tries to be a skeleton for a simple Json API REST. Please, check the readme file for more details.
tags=playframework,scala,api,rest,json,seed
Loading

0 comments on commit 76c5d77

Please sign in to comment.