Grails URL Mappings
Learn to configure routes and endpoints
Authors: Zachary Klein
Grails Version: 5.0.1
1 Getting Started
Whether you’re building a traditional page-based application, a Single-Page App, or a microservice, having a logical, extendable API is a very important part of application design. Grails offers powerful support for building APIs with URL Mappings.
The sample application we are going to build will be a conference scheduling app, called agenda
. We will have a simple domain model consisting of Speakers, Talks, an Agenda (which ties together speakers and talks), and a Conference (which has an agenda). We won’t be creating the domain model in this guide, but we’ll create the web layer that will sit above it for now.
1.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
1.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_url_mappings.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_url_mappings/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/grails_url_mappings/complete
|
2 Creating our Application
Please choose from the following options to create your project:
1. Create a new Application via the Grails CLI
If you have Grails (>= 3.3.0) installed, you can create your application on the command line:
~ grails create-app agenda
2. Create a new Application via the Application Forge
You can download your project from http://start.grails.org, either by using the online wizard, or the following curl
command:
~ curl -O start.grails.org/agenda.zip -d version=3.3.2
3. Use the Initial Project from the Guide Repo
Within the guide directory, copy the initial
project to a directory of your choice, rename it (e.g., agenda
), and follow along with the guide from within that project.
3 Creating Controllers
Before we can define our URL mappings, we need to create some controllers. You can think of them as the "web layer" of our application. Controllers respond to requests (i.e., from a user’s browser, or from a another microservice), obtain data from the model, and return a response, such as an HTML page or a JSON string.
You can create a controller by simply creating a Groovy class under the grails-app/controllers
directory. However, Grails provides a handy command for creating controllers on the command-line.
Because we’re going to create several controllers, let’s use the interactive mode provided by the Grails CLI. Run the following command from within your project directory:
~ ./grailsw
...
grails >
Once the interactive CLI is loaded up, you can run any available Grails commands, including create-controller
. Run the following commands:
grails > create-controller demo.Talk
grails > create-controller demo.Speaker
grails > create-controller demo.Agenda
grails > create-controller demo.Conference
Go head and quit the Grails CLI:
grails > exit
Now that we have our controllers, it’s time to create actions which will be exposed as endpoints for users of our application (whether humans or perhaps a client of our API). Actions are simply methods defined in a controller class. In a CRUD (*C*reate, *R*ead, *U*pdate, *D*elete) controller, you will typically want actions such as shown below:
class CrudController {
def index() {} //usually a "list" view
def save() {} //save new records
def show(Serializable id) {} //retrieve details on a specific record
def update(Serializable id) {} //update a specific record
def delete(Serializable id) {} //delete specific record
}
If this was a REST controller, the above actions might be sufficient for CRUD operations. If we wanted to also display HTML forms for editing and creating records, we might also add create
and edit
actions to display those pages.
Let’s stub out CRUD actions for our (future) domain model. Edit the first three controllers (TalkController
, SpeakerController
, and AgendaController
) as shown below:
package demo
class TalkController {
def index() {
render "Retrieving all Talks..."
}
def save() {
render "Saving new Talk..."
}
def show(Long id) {
render "Retrieving Talk ${id}..."
}
def update(Long id) {
render "Updating Talk ${id}..."
}
def delete(Long id) {
render "Deleting Talk ${id}..."
}
}
package demo
class SpeakerController {
def index() {
render "Retrieving all Speakers..."
}
def save() {
render "Saving new Speaker..."
}
def show(Long id) {
render "Retrieving Speaker ${id}..."
}
def update(Long id) {
render "Updating Speaker ${id}..."
}
def delete(Long id) {
render "Deleting Speaker ${id}..."
}
}
package demo
class AgendaController {
def index() {
render "Retrieving all Agendas..."
}
def save() {
render "Saving new Agenda..."
}
def show(Long id) {
render "Retrieving Agenda ${id}..."
}
def update(Long id) {
render "Updating Agenda ${id}..."
}
def delete(Long id) {
render "Deleting Agenda ${id}..."
}
}
For the ConferenceController
, we’ll create a different set of actions - talks
, speakers
, and agenda
. In addition, the talks
and speakers
actions will accept an optional id
parameter, so that we can display a specific speaker or talk.
Edit ConferenceController.groovy
as shown below:
package demo
class ConferenceController {
def talks(Long id) {
if(id) {
render "Returning conference talk ${id}..."
} else {
render "Returning conference talks..."
}
}
def speakers(Long id) {
if(id) {
render "Returning conference speaker ${id}..."
} else {
render "Returning conference speakers..."
}
}
def agenda() {
render "Returning conference agenda..."
}
}
Now that we have our controller actions stubbed out, let’s see what Grails URL mappings have given us out of the box.
4 Defining URL Mappings
Let’s look at the default UrlMappings.groovy
file (under grails-app/controllers/demo
):
package demo
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
The very first mapping in the mappings
block is pretty straightforward. It has two components: a string, defining the URL pattern, and a closure, where options (such as constraints) can be specified to change the behavior of the mapping.
See how to use URL constraints here in the Grails documentation. |
The URL pattern can include variables, which are denoted by Groovy Strings within the pattern (e.g., $controller
or ${controller}
. Grails has a few special wildcards which it supports out of the box. For example, $controller
will be mapped to the name of a given controller, $action
will be mapped to an action within that controller, and so forth. Optional variables are suffixed with a ?
. E.g., /controller/$action?
Using the variables for all the parts of the URL pattern means this particular mapping will match any URL. See the table below:
URL | Result |
---|---|
|
|
|
|
|
|
In addition to mapping to controller actions, URL mappings can also map directly to a view (without specifying a controller or action), or an arbitrary URI (useful for redirects). For example, the default URL mapping for the root context, /
, points to the default index.gsp
page under grails-app/views
:
"/"(view:"/index")
You can use the uri
argument to map to a different URI:
"/hello"(uri: "/hello.dispatch")
Another useful kind of mapping are HTTP error code mappings, also seen in the default UrlMappings.groovy
file. The default mappings simply load a default error view, however you could easily redirect these requests to an error-handling or logging controller to apply custom error handling logic and recovery in your app. E.g.,
"500"(controller:'myErrorController', action: 'somethingBroke')
"404"(controller:'myErrorController', action: 'notFound')
The default URL mappings will cover most of our needs in this sample app. However, to give us finer grained control, we are going to add a few of our own mappings for the ConferenceController
actions we stubbed out above.
The default set of URL mappings cover many use cases. However, when it’s time to secure your application, you may want to remove the default mapping and replace it with more specific mappings (so that users cannot access any controller action they like simply by figuring out the names). |
Edit the UrlMappings.groovy
file as shown below:
package demo
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/talks/$id?"(controller: 'conference', action: 'talks') (1)
"/speakers/$id?"(controller: 'conference', action: 'speakers')
"/agenda"(controller: 'conference', action: 'agenda') (2)
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
1 | We are mapping a custom URL pattern /talks/$id? to our ConferenceController actions. The $id variable means that this component of the URL pattern will be mapped to the id parameter of the talks action. The suffix of ? makes this variable optional. |
2 | Note that we’re not using any variables in this patterns, so the request must match the URL pattern exactly. Otherwise, it will be handled by the default URL mapping at the top of the file. |
If you start up the application now and make a request to http://localhost:8080/talks
, you should see this text rendered to the browser:
Returning conference talks...
Try another URL: http://localhost:8080/talks/1
:
Returning conference talk 1...
HTTP Methods
There’s one more feature to note on URL mappings before we move on. In addition to specifying URL patterns and constraints, mappings can also specify the specific HTTP method required to match the mapping. This is as simple as prefixing the pattern with the required HTTP verb, such as get
, post
, or put
.
Let’s update our UrlMappings.groovy
file one more time, to specify that we only accept GET
requests to our ConferenceController
actions.
package demo
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
get "/talks/$id?"(controller: 'conference', action: 'talks')
get "/speakers/$id?"(controller: 'conference', action: 'speakers')
get "/agenda"(controller: 'conference', action: 'agenda')
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
If you now use curl
(or another HTTP client) to make a non-GET request to one of these URLs, you’ll receive a 404 page. Try it with the below curl
command:
~ curl -X PUT localhost:8080/talks
5 Organize URL Mappings
Over time, your applications might end up with many similar URL mappings, or large numbers of mappings that are specific to one part of the app. There are a few ways to organize URL mappings, including the group
method, as well as using multiple UrlMapping.groovy
files.
URL Groups
If you have a large number of mappings that fall under a particular path, you can use the group
method to specify the shared portion of the URL pattern, and list the right-hand portion of each pattern separately.
Edit the UrlMappings.groovy
file again, and group the three URL mappings we’ve added under a shared path: /conf
.
package demo
class UrlMappings {
static mappings = {
//...
group "/conf", {
get "/talks/$id?"(controller: 'conference', action: 'talks')
get "/speakers/$id?"(controller: 'conference', action: 'speakers')
get "/agenda"(controller: 'conference', action: 'agenda')
}
//...
}
}
If you restart the application, you can see that these mappings now require /conf
as the first part of the URL pattern. E.g., http://localhost:8080/conf/talks
Multiple UrlMappings files
Grails supports multiple UrlMappings
files. The only requirements are that each file must be uniquely named, reside within grails-app/controllers
, and the file name must end with UrlMappings.groovy
.
Let’s create a second UrlMappings
file for our ConferenceController
mappings. Create a new file named ConferenceUrlMappings.groovy
, end edit its contents as shown below:
package demo
class ConferenceUrlMappings {
static mappings = {
group "/conf", {
get "/talks/$id?"(controller: 'conference', action: 'talks')
get "/speakers/$id?"(controller: 'conference', action: 'speakers')
get "/agenda"(controller: 'conference', action: 'agenda')
}
}
}
Now, delete the /conf
URL mappings from the original UrlMappings.groovy
file, so it matches the version below:
package demo
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
6 Testing URL Mappings
The Grails Testing Support library is the recommended way to write tests for most Grails artefacts, including URL mappings. The library is included in the current version of Grails, and it provides a UrlMappingsUnitTest
trait that makes it easy to test URL mappings.
Traits are a powerful feature of the Groovy programming language, and many Grails features utilize them. You can create your own traits as well as part of your application code. See the Groovy documentation for more information. |
Let’s write a unit test for our ConferenceUrlMappings
. Create a new Groovy file under src/test/groovy/demo/
, named ConferenceUrlMappingsSpec.groovy
. Edit the file with the content below:
package demo
import grails.testing.web.UrlMappingsUnitTest
import spock.lang.Specification
class ConferenceUrlMappingsSpec extends Specification implements UrlMappingsUnitTest<ConferenceUrlMappings> { (1)
void setup() {
mockController(ConferenceController) (2)
}
void "test conference mappings"() {
expect: "calls to /conference/talks to succeed"
verifyUrlMapping("/conf/talks", controller: 'conference', action: 'talks', method: 'GET') (3)
verifyUrlMapping("/conf/talks/1", controller: 'conference', action: 'talks', method: 'GET') {
id = '1'
}
when: "calling /conf/speakers"
assertUrlMapping("/conf/speakers", controller: 'conference', action: 'speakers', method: 'GET') (4)
then: "no exception is thrown"
noExceptionThrown()
}
}
1 | Our test is a standard Spock Specification , which then implements the UrlMappingsUnitTest trait (provided by the testing support library). Note that the trait accepts a generic, which is the URL mapping class that we wish to test; in this case, ConferenceUrlMappings |
2 | The testing library provides mocking helpers, such as mockController to wire up an instance of a controller for testing. |
3 | The UrlMappingsUnitTest trait provides several testing methods, including verifyUrlMapping , which simply returns true or false if the URL mapping matches the expected controller/action. |
4 | Another test method is assertUrlMapping . This performs the same check as verifyUrlMapping , but it allows you to use the method as part of assertion (such as a Spock when /then block). |
7 Conclusion
At this point you should be comfortable with the concept of URL mappings, and you have a good handle on the most important features. You can learn much more about the capabilities of URL mappings from the Grails Documentation, including namespaces, RESTful resource mappings, constraints, and wildcards.