Show Navigation

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:

or

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 the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-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-app/conf/application.yml
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:

grails-app/domain/demo/Product.groovy
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:

grails-app/domain/demo/Product.groovy
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:

grails-app/domain/demo/Product.groovy
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 the range 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.

src/test/groovy/demo/ProductSpec.groovy
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

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:

grails-app/controllers/demo/ProductController.groovy
@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:

grails-app/controllers/demo/UrlMappings.groovy
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:

grails-app/controllers/demo/UrlMappings.groovy
"/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:

grails-app/controllers/demo/ProductController.groovy
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:

grails-app/controllers/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:

src/test/groovy/demo/ProductControllerSpec.groovy
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:

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 the respond method in a controller.

  • grails-app/views/product/show.gson - This view will be rendered when a single Product instance is rendered via the respond method in a controller.

  • grails-app/views/product/_product.gson - This is the template used by both the index.gson and show.gson views to actually display the data.

The contents of the _product.gson by default look like:

grails-app/views/product/_product.gson
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:

grails-app/views/product/_product.gson
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.

5 Do you need help with Grails?

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team