What is Grails?
Grails is web framework for the JVM built with Groovy on Spring.
Before you start
You'll need to download and install Grails. For this tutorial I've use Grails 1.2.2, but it should work with Grails 1.1.x too.
Test your installation by entering the following into a terminal:
$ grails help
After a little bit of whirring you should see a welcome banner and help.
You might also consider setting up your editor or IDE, but it's not necessary to complete this tutorial.
Create the application
We'll make a small Grails application to serve as the backend for the Todos application.
Change into your projects or source directory and enter the following commands into a terminal:
$ grails create-app todos-backend
Be sure to do this outside of your SproutCore application folder.
This will create a folder called todos-backend containing a template Grails application. By default an in memory HSQLDB data source has been configured so we can start building our application right away.
Create the model
Grails models (domain classes in Grails parlance) are essentially a normal Groovy class, but behind the scenes use GORM (Grails object relational mapping) which is build on top of Hibernate3.
To create our model:
$ cd todos-backend
$ grails create-domain-class sc.Task
This will create a Task domain class in the sc namespace and generate some unit test boilerplate.
Open [path to project]/todos-backend/grails-app/domain/sc/Task.groovy in your editor and add the following:
package sc
class Task {
String description
Integer order = 0
Boolean isDone = false
static constraints = {
}
static mapping = {
order column: "task_order"
}
}
Note: we map the order property to the task_order column because HSQLDB (and many other databases) will choke on column name that's a keyword.
Next we'll bootstrap some data. Open [path to project]/todos-backend/grails-app/conf/BootStrap.groovy and add the following:
import sc.Task
class BootStrap {
def init = { servletContext ->
new Task(description: 'Finish the SproutCore Todos Intro').save()
new Task(description: 'Flip my startup to Google').save()
}
def destroy = {
}
}
To ensure we're on the right track let's run the tests:
$ grails test-app
The empty unit test created with the Task domain class will pass (it tests nothing, a trap which can test you into believing that your coverage is better than it really is!!), but what we're interested in is the bootstrapping of the database before the integration tests are run: you shouldn't get a stacktrace.
Create the controller
A controller handles requests and creates or prepares the response (or delegate the response to a view).
To create the controller:
$ grails create-controller sc.Task
This will generate a controller class called TaskController and some boilerplate test code.
Before we start work on the controller we need to map the REST URIs and request method to actions of the controller.
Open [path to project]/todos-backend/grails-app/conf/UrlMappings.groovy and edit:
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?" {
constraints {
// apply constraints here
}
}
"/"(view: "/index")
"500"(view: '/error')
"/tasks/$id?"(controller: "task") {
action = [GET: "show", PUT: "update", DELETE: "delete", POST: "save"]
}
}
}
This maps HTTP GET to TaskController.show, PUT to TaskController.update etc. and the passes id as a parameter to each action (if supplied).
Next open [path to project]/todos-backend/grails-app/controllers/sc/TaskController.groovy and add the following:
package sc
import grails.converters.JSON
class TaskController {
def task2map = {t ->
[guid: "/todos-backend/tasks/$t.id",
description: t.description,
order: t.order ?: 0,
isDone: t.isDone]
}
def list = {
def tasks = Task.list()
render(contentType: "text/json") {
content = array {
tasks.each {task(task2map(it))}
}
}
}
def show = {
if (params.id) {
def task = Task.get(params.id)
if (task) {
render(contentType: "text/json") {
content(task2map(task))
}
}
else {
render text: "${params.id} not found.", status: 404
}
}
else {
list()
}
}
def delete = {
def task = Task.get(params.id)
if (task) {
task.delete()
render "" //would normally return 204 No Content here, but sc-server barfs on 0 bytes.
}
else {
render text: "Task ${params.id} not found.", status: 404
}
}
def save = {id=null ->
def payload = JSON.parse(request.reader.text)
def task = id ? Task.get(id) : new Task()
if (task) {
task.properties = payload
if (task.save()) {
response.setHeader('Location', "/todos-backend/tasks/$task.id")
render text: "", status: 201
}
else {
render text: "Task could not be saved.", status: 500
}
}
else {
render text: "Task ${params.id} not found.", status: 404
}
}
def update = {
save(params.id)
}
}
The task2map closure is a convenience method to aid serialising the model into JSON (there are nicer ways to do this but they're beyond the scope of this tutorial).
As a matter of taste I've rolled update into save, and in doing so return the location header and status 201 for both the POST and PUT. I'd typically return a 204 for DELETE instead of a 200 too, however sc-server strenuously objects. You should rework things to suit your tastes (or just flame me in the comments!).
Starting the server
Depending on your version of Grails the application will start in a development instance of either jetty or tomcat. To start:
$ grails run-app
Browse to http://localhost:8080/todos-backend/tasks and you should see your bootstrap tasks serialized as JSON.
You can also test the other methods with curl or HTTP Client.
Setting up the proxy
Add the following line to the Buildfile in your SproutCore project:
proxy "/todos-backend/tasks", :to => "localhost:8080"
Note the application name in the path. In Todos 07-Hooking Up to the Backend you'll need to modify the calls to
SC.Request.getX('/tasks').json() in fetch and createRecord to
SC.Request.getUrl('/todos-backend/tasks').json()
and
SC.Request.postUrl('/todos-backend/tasks').json()
respectively.
Next step
Continue to the next step: Step 7: Hooking Up to the Backend ยป
Comments (1)
Jeff Potts said
at 1:32 pm on Jan 7, 2011
Worked the first time with no adjustments. Thanks for the write up!
You don't have permission to comment on this page.