Grails with Micronaut HTTP Client
In this guide, we will learn how to use the Micronaut HTTP Client in a Grails app.
Authors: Nirav Assar
Grails Version: 5.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 learn how to execute an HTTP client to make external rest API calls as well perform spock testing. You will use Micronaut’s HTTP Client, which has both a low-level API and a higher level AOP-driven API.
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
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/grails-micronaut-http.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/grails-micronaut-http/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/grails-micronaut-http/complete
|
3 Writing the Application
The /initial
folder is a grails app created with the rest-api
profile. It is a simple app that queries the
Apple iTunes API.
The app takes in a searchTerm
and is layered with:
-
example.grails.SearchController
-
example.grails.ItunesSearchService
The call is stubbed out until we implement it in this guide.
Verify the functionality by running ./gradlew bootRun
and then going to the URL http://localhost:8080/search/searchWithApi?searchTerm=U2
.
You should see dummy data JSON returned.
4 Micronaut HTTP Client
The Micronaut HTTP Client dependency is included in the grails rest-api
profile and replaces the datastore-rest-client
.
As of Grails 4, it is the recommended HTTP Client for Grails. (The old HTTP client is still backward compatible in Grails 4).
In our music app we will be using the Micronaut HTTP client in both service functionality and testing so we must change
the qualifier in build.gradle
to compile
:
dependencies {
...
implementation "io.micronaut:micronaut-http-client"
}
4.1 Use the Low-Level Micronaut HTTP Client API
We will query the iTunes repository for the artist and expect to receive several pieces of information in the form of JSON, such
as album name and a URL link to the album. In order to capture the return data easily, we can create POJOs with properties
named exactly the same as the JSON structure. The iTunes API will return resultCount
with a list of albums that contain:
-
artistName
-
collectionName
-
collectionViewUrl
Note the following POJOs are already created:
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
class SearchResult {
int resultCount
List<Album> results = []
}
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
class Album {
String artistName
String collectionName
String collectionViewUrl
}
We need to configure Micronaut to accept the text/javascript
MIME type from the iTunes API.
---
micronaut:
codec:
json:
additionalTypes:
- text/javascript
In the ItunesSearchService
, we create the low level client and use the API to contact iTunes.apple.com
. Conceptually we are
going to create a client with a baseUrl
, then form a request object that sends a GET
request with URL params.
Subsequently, we will issue a blocking request and received a String response. ObjectMapper
from Jackson Databind will
map the incoming JSON to the POJOs. Add the following to ItunesSearchService
:
List<Album> searchWithApi(String searchTerm) {
String baseUrl = "https://itunes.apple.com/"
HttpClient client = HttpClient.create(baseUrl.toURL())
HttpRequest request = HttpRequest.GET(UriBuilder.of('/search')
.queryParam('limit', 25)
.queryParam('media', 'music')
.queryParam('entity', 'album')
.queryParam('term', searchTerm)
.build())
HttpResponse<String> resp = client.toBlocking().exchange(request, String) (1)
String json = resp.body()
ObjectMapper objectMapper = new ObjectMapper()
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) (2)
SearchResult searchResult = objectMapper.readValue(json, SearchResult) (3)
searchResult.results
}
1 | Issue the request with a blocking call, and accept a String response. |
2 | Ignore extra properties. |
3 | Map the JSON into the POJOs. |
Run ./gradlew bootRun
and hit http://localhost:8080/search/searchWithApi?searchTerm=U2
and see the results.
4.2 Use the Declarative Micronaut HTTP Client
The same functionality can be implemented with Micronaut’s Declarative HTTP Client. The @Client
annotation can be declared
on an interface and at compilation time a client will be created for you. The interface method can be declared with a @Get
and return
the data-binded POJO. This simple and elegant approach cuts down on much of the code in the previous example.
Create a declarative client:
package example.grails
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@Client("https://itunes.apple.com/") (1)
interface ItunesClient {
@Get("/search?limit=25&media=music&entity=album&term={term}") (2)
SearchResult search(String term)
}
1 | Declare with @Client and set the URL. |
2 | Define a @Get request with parameters. |
Inject the client into the service, and add the call to the interface method.
...
@Autowired
ItunesClient itunesClient
List<Album> searchWithDeclarativeClient(String searchTerm) {
SearchResult searchResult = itunesClient.search(searchTerm)
searchResult.results
}
...
Add a controller method that routes to the service method:
...
def searchWithDeclarativeClient(String searchTerm) {
if(searchTerm) {
List<Album> albums = itunesSearchService.searchWithDeclarativeClient(searchTerm)
respond([searchTerm: searchTerm, albums: albums])
}
}
...
Run ./gradlew bootRun
and hit http://localhost:8080/search/searchWithDeclarativeClient?searchTerm=U2 and see the results.
5 Test with Micronaut HTTP Client
The Micronaut Client can also be used for testing purposes with Spock. In our music app let’s create a domain object to capture record labels. We will use the grails built-in REST API for domain classes.
Create a domain object and tag it with @Resource
and a given URI. This will expose REST functionality for the object.
package example.grails
import grails.rest.Resource
@Resource(uri='/recordlabels')
class RecordLabel {
String name
}
In the BootStrap.groovy
, add some seed data for testing purposes:
package example.grails
import grails.gorm.services.Service
@Service(RecordLabel)
interface RecordLabelService {
RecordLabel save(String name)
}
package example.grails
import groovy.transform.CompileStatic
@CompileStatic
class BootStrap {
RecordLabelService recordLabelService
def init = { servletContext ->
recordLabelService.save("Warner")
recordLabelService.save("Sony")
}
def destroy = {
}
}
Let’s create an integration test suite that first does a GET
request to retrieve the existing list of RecordLabels. The test will
also issue a POST
to insert one more record label, with a map of data passed in. In both tests, we will verify that the response comes back correctly.
package example.grails
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import grails.testing.spock.OnceBefore
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import spock.lang.Shared
import spock.lang.Specification
@Integration
@Rollback
class RecordLabelControllerSpec extends Specification {
@Shared
HttpClient client
@OnceBefore
void init() { (1)
String baseUrl = "http://localhost:$serverPort"
this.client = HttpClient.create(baseUrl.toURL())
}
void "test rest get record labels"() {
when:"record labels exist"
HttpResponse<List<Map>> resp = client.toBlocking().exchange(HttpRequest.GET("/recordlabels"), Argument.of(List, Map)) (2)
then: "client can retrieve them"
resp.status == HttpStatus.OK (3)
resp.body().size() == 2
resp.body()[0].name == "Warner"
resp.body()[1].name == "Sony"
}
void "test rest post record labels"() {
when:"a post is issued"
HttpResponse<Map> resp = client.toBlocking().exchange(HttpRequest.POST("/recordlabels", [name: "Universal"]), Map) (4)
then: "element is created"
resp.status == HttpStatus.CREATED (5)
resp.body().size() == 2
resp.body().id
resp.body().name == "Universal"
}
}
1 | Initialize the client once for all tests. Give it a URL with serverPort which is the assigned port of the integration test. |
2 | Issue a GET . The expected body type will be a List of Maps. |
3 | Verify the status and body elements of the JSON. |
4 | Send a map of data to the POST , which will be interpreted as JSON. |
5 | Verify the status and returned data for the test. |
Test the app with ./gradlew check
.
6 Running the Application
To run the application use the ./gradlew bootRun
command which will start the application on port 8080.