Building a REST application with MongoDB
This guide will demonstrate how you can use Grails, GORM and MongoDB to build a REST application
Authors: Graeme Rocher
Grails Version: 4.0.1
1 Grails Training
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
In this guide you are going to build a Grails application that uses GORM for MongoDB to access MongoDB and produce a JSON response in a RESTful manner.
2.1 What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately -
An installation of MongoDB 3.0.0 or above
2.2 How to complete the guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/rest-mongodb.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/rest-mongodb/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/rest-mongodb/complete
|
Alternatively, if you already have Grails 4.0.1 installed then you can create a new application using the following command in a Terminal window:
$ grails create-app mongodb-example --profile rest-api --features mongodb
$ cd mongodb-example
When the create-app
command completes, Grails will create a mongodb-example
directory with an application configured to create a REST application by default (using the rest-api
profile) and configured to use the mongodb
feature.
Once you have the application create you need to startup MongoDB. Typically this is done via the MONGODB_HOME/bin/mongod
executable:
$ sudo ./mongod
2016-11-22T12:42:30.569+0100 I CONTROL [initandlisten] db version v3.0.4
2016-11-22T12:42:30.569+0100 I CONTROL [initandlisten] git version: 0481c958daeb2969800511e7475dc66986fa9ed5
2016-11-22T12:42:30.569+0100 I CONTROL [initandlisten] build info: Darwin mci-osx108-11.build.10gen.cc 12.5.0 Darwin Kernel Version 12.5.0: Sun Sep 29 13:33:47 PDT 2013; root:xnu-2050.48.12~1/RELEASE_X86_64 x86_64 BOOST_LIB_VERSION=1_49
2016-11-22T12:42:30.569+0100 I CONTROL [initandlisten] allocator: system
2016-11-22T12:42:30.569+0100 I CONTROL [initandlisten] options: {}
2016-11-22T12:42:31.297+0100 I NETWORK [initandlisten] waiting for connections on port 27017
As you can see MongoDB is by default available on port 27017
.
3 Writing the Application
Now you are ready to start writing the application.
3.1 Configure the Application
The first step after starting the MongoDB server is to ensure the application is correctly configured. By default the configuration for the MongoDB client can be found in the grails-app/conf/application.yml
file:
grails:
mongodb:
host: localhost
port: 27017
#username: ""
#password: ""
#databaseName: "mydatabase"
The configuration is setup to use the defaults, however if your MongoDB server requires authentication or any custom configuration you may want to alter the configuration.
3.2 Create a Domain Class
Each MongoCollection is represented by a GORM domain class.
You can create a domain class with the create-domain-class
CLI command from the root of your project:
$ ./grailsw create-domain-class Product
| Created grails-app/domain/demo/Product.groovy
| Created src/test/groovy/demo/ProductSpec.groovy
Alternatively, you can equally create a domain class with your favourite text editor or IDE.
A domain class is a simple Groovy class and you can represent each attribute of a MongoDB Document using a property. For example:
package demo
import grails.compiler.GrailsCompileStatic
@GrailsCompileStatic
class Product {
}
Each domain class defined in the application will be compiled to implement the MongoEntity trait. If you prefer you can define this explicitly:
import grails.mongodb.*
class Product implements MongoEntity<Product> {
..
}
3.3 Apply Domain Constraints
If you wish to define validation constraints
on properties defined in a GORM domain class, you can do so using the constraints
property:
static constraints = {
name blank: false
price range: 0.0..1000.00
}
The above example applies two constraints:
-
The
name
property is constrained so that it cannot be a blank string. -
The
price
property is constrained so that it must be greater than 0 and less than a thousand using therange
constraint.
To verify these constraints work to our expectation you can write a test. If you used the create-domain-class
a test
called src/test/groovy/demo/ProductSpec.groovy
was generated for you already. Otherwise simply creating an appropriately
named test in src/test/groovy
using your IDE or text editor will suffice.
Tests in Grails are written with Spock Framework, tests including having spaces in the test name.
To write unit tests with MongoDB and Spock, you can simply extend from grails.test.mongodb.MongoSpec
.
import grails.test.mongodb.MongoSpec
import grails.testing.gorm.DomainUnitTest
import com.github.fakemongo.Fongo
import com.mongodb.MongoClient
class ProductSpec extends MongoSpec (3)
implements DomainUnitTest<Product> { (4)
@Override
MongoClient createMongoClient() { (2)
new Fongo(getClass().name).mongo
}
void 'a negative value is not a valid price'() { (1)
given:
domain.price = -2.0d
expect:
!domain.validate(['price'])
}
void 'a blank name is not save'() { (1)
given:
domain.name = ''
expect:
!domain.validate(['name'])
}
void 'A valid domain is saved'() { (1)
given:
domain.name = 'Banana'
domain.price = 2.15d
when:
domain.save()
then:
Product.count() == old(Product.count()) + 1
}
}
1 | Spock allows you to define much more readable |
2 | Override the createMongoClient method of the MongoSpec base class. Use something such as Fongo; an in-memory java implementation of MongoDB. |
3 | MongoSpec is an abstract class that will initialise GORM in the setup phase of the specification being executed |
4 | Use the grails.testing.gorm.DomainUnitTest trait to unit test single domain class. |
Add the fongo dependency to build.gradle
testCompile 'com.github.fakemongo:fongo:2.1.0'
Now to run the test you can run ./gradlew check
from a terminal window or run the test within your IDE if the IDE supports it:
$ ./gradlew check
Checkout How to test domain class constraints? guide to learn more about testing constraints.
3.4 Create a Controller
With the data model defined now it time to write a controller.
The quickest way to do that is with the create-restful-controller
command from a terminal window:
$ ./grailsw create-restful-controller demo.Product
| Created grails-app/controllers/demo/ProductController.groovy
However, you can also simply create the controller with your favourite text editor or IDE.
The contents of the controller should look like the following:
@CompileStatic
class ProductController extends RestfulController {
static responseFormats = ['json', 'xml']
ProductController() {
super(Product)
}
}
The RestfulController
super class will implement all the necessary operations to perform the common REST verbs such as GET
, POST
, PUT
and DELETE
. If is you wish to override or forbid certain verbs you can override the equivalent method from the super (for example the delete
method for DELETE
) to return an alternative or forbidden response.
3.5 Map the Controller to a URI
By default the controller will be exposed under the /product
URI. This is due to the default grails-app/conf/demo/UrlMappings.groovy
class:
delete "/$controller/$id(.$format)?"(action: 'delete')
get "/$controller(.$format)?"(action: 'index')
get "/$controller/$id(.$format)?"(action: 'show')
post "/$controller(.$format)?"(action: 'save')
put "/$controller/$id(.$format)?"(action: 'update')
patch "/$controller/$id(.$format)?"(action: 'patch')
As you can see above each HTTP verb is mapped such that the controller name is established from the URI itself using the $controller
syntax.
If you wish to use another name or be explicit about the URI used you can define an additional mapping uses the resources
mapping:
"/products"(resources:"product")
In this case the controller will be mapped to /products
instead of /product
.
3.6 Implement a Search Endpoint
If you wish to add an additional endpoint to your REST API then it is simply a matter of implementing the corresponding action.
For example, say you wanted to be able to search for products using the /products/search
URI and a query. To do so the first step is to implement a search
action in the controller:
def search(String q, Integer max ) { (1)
if (q) {
def query = Product.where { (2)
name ==~ ~/$q/
}
respond query.list(max: Math.min( max ?: 10, 100)) (3)
}
else {
respond([]) (4)
}
}
1 | An action called search is defined that takes a query parameter, called q , and a max parameter |
2 | A where query is executed that uses a regular expression (regex) to search for products. |
3 | The respond method is used to respond with a list of results |
4 | For the case where no query is specified we respond with an empty list |
With the action written you now need to expose the /products/search
endoint by defining the appropriate mapping in grails-app/conf/demo/UrlMappings.groovy
:
'/products'(resources: 'product') {
collection {
'/search'(controller: 'product', action: 'search')
}
}
The above example uses the collection
method to nest URIs directly underneath the /products
URI (for example /product/search
) instead of nesting it below the resource identifier (for example /product/1/search
).
See the Grails user guide on Mapping REST Resources for more information on controllering how URIs map to controllers. |
3.7 Testing the Search Endpoint
To write a unit test for the search
action your can use the create-unit-test
command of the CLI to create one:
$ ./grailsw create-unit-test demo.ProductController
Or alternatively just create a src/test/groovy/demo/ProductControllerSpec.groovy
file using your favourite text editor or IDE.
Write the next unit test:
import com.github.fakemongo.Fongo
import com.mongodb.MongoClient
import grails.test.mongodb.MongoSpec
import grails.testing.web.controllers.ControllerUnitTest
class ProductControllerSpec extends MongoSpec (1)
implements ControllerUnitTest<ProductController> { (2)
@Override
MongoClient createMongoClient() { (3)
new Fongo(getClass().name).mongo
}
void setup() { (4)
Product.saveAll(
new Product(name: 'Apple', price: 2.0),
new Product(name: 'Orange', price: 3.0),
new Product(name: 'Banana', price: 1.0),
new Product(name: 'Cake', price: 4.0)
)
}
void 'test the search action finds results'() {
when: 'A query is executed that finds results'
controller.search('pp', 10)
then: 'The response is correct'
response.json.size() == 1
response.json[0].name == 'Apple'
}
}
1 | MongoSpec is an abstract class that will initialise GORM in the setup phase of the specification being executed |
2 | Override the createMongoClient method of the MongoSpec base class. Use something such as Fongo; an in-memory java implementation of MongoDB. |
3 | Use the grails.testing.web.controllers.ControllerUnitTest trait to unit test controllers. |
4 | The setup method uses the saveAll method to save multiple domain classes to serve as test data. |
The test performs a search by executing the search
method, passing appropriate arguments and verifying the JSON written to the response.
The tests asserts the value of the json
property of the response
. The resulting JSON rendered by Grails looks like:
[{"id":1,"name":"Apple","price":2.0}]
Let’s take a look at how we can customize this JSON.
3.8 Customizing the JSON Output
Grails uses JSON Views to represent render JSON responses. The idea being to continue the philosophy of separating the controller logic from the view logic.
The default view rendered if no specific view is found is grails-app/views/object/_object.gson
:
import groovy.transform.*
@Field Object object
json g.render(object)
The default _object.gson
view simply uses the g.render(..)
method to automatically product a JSON representation of the object.
If you want to alter the output of the JSON the way that is done in Grails is by creating a view. You can generate a starting point with the generate-views
command of the CLI:
./grailsw generate-views demo.Product
| Rendered template index.gson to destination grails-app/views/product/index.gson
| Rendered template show.gson to destination grails-app/views/product/show.gson
| Rendered template _domain.gson to destination grails-app/views/product/_product.gson
As you can see 3 templates were generated:
-
grails-app/views/product/index.gson
- This view will be used when a collection (typically a list of results from GORM) is rendered via therespond
method in a controller. -
grails-app/views/product/show.gson
- This view will be rendered when a singleProduct
instance is rendered via therespond
method in a controller. -
grails-app/views/product/_product.gson
- This is the template used by both theindex.gson
andshow.gson
views to actually display the data.
The contents of the _product.gson
by default look like:
import demo.Product
model {
Product product
}
json g.render(product)
The call to g.render(product)
outputs all properties.
However, the json
property is an instance of Groovy’s StreamingJsonBuilder and you can use it to alter the output as per your needs.
For example:
import demo.Product
model {
Product product
}
Currency currency = locale?.country ? Currency.getInstance(locale) : Currency.getInstance('USD')
json {
id product.id
name product.name
price "${currency.symbol}${product.price}"
}
In this trivialized example, we output the currency symbol based on the user’s locale. Now the resulting JSON will look like:
[{id:1,"name":"Apple","price":"$2.0"}]
JSON Views are very flexible, for more information on customizing the output to your needs see the documentation. |
4 Running the Application
To run the application use the ./gradlew bootRun
command which will start the application on port 8080.
4.1 Creating Data with POST
Once the application has started up you can create a Product
instance using your preferred HTTP client. In the following examples we will be using the curl.
To submit a POST
request use the following in a terminal window:
$ curl -i -H "Content-Type:application/json" -X POST localhost:8080/products -d '{"name":"Orange","price":2.0}'
The resulting output will be:
HTTP/1.1 201
X-Application-Context: application:development
Location: http://localhost:8080/products/2
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:46:40 GMT
{"name":"Orange","price":"$2.0"}
As you can see an HTTP 201 status code is returned.
4.2 Reading Data with GET
You can read all of the Product
instances back using a GET
request:
$ curl -i localhost:8080/products
Or read only a single instance by id
:
$ curl -i localhost:8080/products/1
Which will return:
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:50:58 GMT
{"id":1,"name":"Orange","price":"$2.0"}
4.3 Updating Data with PUT
To update data you can use a PUT
request with the id and in the URI and data you want to change:
$ curl -i -H "Content-Type:application/json" -X PUT localhost:8080/products/1 -d '{"price":3.0}'
In this case the resulting output will be:
HTTP/1.1 200
X-Application-Context: application:development
Location: http://localhost:8080/products/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:52:14 GMT
{"id":1,"name":"Orange","price":"$3.0"}
If you were to attempt update the data with an invalid value:
$ curl -i -H "Content-Type:application/json" -X PUT localhost:8080/products/1 -d '{"price":-2.0}'
Then an error response will be received:
HTTP/1.1 422
X-Application-Context: application:development
Location: http://localhost:8080/products/1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 Nov 2016 08:54:25 GMT
{"message":"Property [price] of class [class demo.Product] with value [-2] does not fall within the valid range from [0] to [1,000]","path":"","_links":{"self":{"href":"http://localhost:8080/products/1"}}}
The error response is rendered by the grails-app/views/error.gson view and the messages are obtained from the message bundles located at grails-app/i18n
|
4.4 Deleting Data with DELETE
To delete a Product
simply send a DELETE
request:
$ curl -i -X DELETE localhost:8080/products/1
If deleting the instance was successful the output will be:
HTTP/1.1 204
X-Application-Context: application:development
Content-Type: application/json;charset=UTF-8
Date: Wed, 23 Nov 2016 08:57:27 GMT
Congratulations! You have built your first REST application with Grails, GORM and MongoDB!
Remember you can obtain the source code for the completed examples using the links on the right. |