--- name: grails-developer description: Comprehensive guide for Grails development, covering web applications, REST APIs, GORM, controllers, services, views, plugins, and testing with Spock and Geb license: Apache-2.0 --- ## What I Do - Provide detailed guidance for building Grails web applications and REST APIs. - Assist with GORM for data modeling, controllers for request handling, services for business logic, and views (GSP, JSON, Markup). - Support testing with Spock (unit/integration tests) and Geb for browser automation. - Guide plugin usage and development, security implementation, and deployment strategies. - Help with configuration, internationalization, async programming, and performance optimization. ## When to Use Me Activate this skill when developing with Grails, including: - Building CRUD applications, RESTful APIs, or full-stack web applications. - Working with GORM domain classes, constraints, and relationships. - Creating controllers, services, interceptors, or tag libraries. - Writing Spock specifications or Geb functional tests. - Configuring plugins, security, caching, or database migrations. - Deploying Grails applications to containers or cloud platforms. ## Technology Stack Grails is built on: - **Spring Boot**: 4.1.x - **Spring Framework**: 7.0.x - **Groovy**: 5.1.x - **Gradle**: 9.7 or later - **Spock**: 2.4-groovy-5.0 - **Jakarta EE**: 10 (migrated from javax.*) - **Micronaut**: Optional via `grails-micronaut` plugin ## Project Structure ``` myapp/ ├── grails-app/ │ ├── conf/ # Configuration │ │ ├── application.yml # Main config │ │ ├── logback-spring.xml # Logging config │ │ └── spring/resources.groovy # Spring bean definitions │ ├── controllers/ # Request handlers │ ├── domain/ # GORM domain classes │ ├── i18n/ # Message bundles │ ├── init/ # Bootstrap classes │ ├── services/ # Business logic │ ├── taglib/ # Custom GSP tags │ ├── utils/ # Utility classes │ └── views/ # GSP templates ├── src/ │ ├── main/groovy/ # Additional Groovy classes │ ├── main/java/ # Java classes │ ├── test/groovy/ # Test specifications │ └── integration-test/groovy/ # Integration test specifications ├── build.gradle # Build configuration └── gradle.properties # Project properties ``` ## Domain Classes (GORM) ### Basic Domain Class ```groovy class Book { String title String author Date datePublished Integer pages static constraints = { title blank: false, maxSize: 255 author blank: false datePublished nullable: true pages min: 1, nullable: true } static mapping = { table 'books' title column: 'book_title' datePublished type: 'date' } } ``` ### Relationships ```groovy class Author { String name static hasMany = [books: Book] static mapping = { books cascade: 'all-delete-orphan' } } class Book { String title static belongsTo = [author: Author] } // Many-to-Many class Book { static hasMany = [categories: Category] static belongsTo = Category } class Category { String name static hasMany = [books: Book] } ``` ### GORM Queries ```groovy // Dynamic finders Book.findByTitle("Grails Guide") Book.findAllByAuthorLike("%Smith%") Book.findByTitleAndAuthor("Guide", "Smith") Book.countByPagesGreaterThan(100) // Where queries Book.where { title =~ "%Grails%" && pages > 100 }.list() // Criteria queries Book.withCriteria { like('title', '%Grails%') gt('pages', 100) order('datePublished', 'desc') maxResults(10) } // HQL queries Book.executeQuery( "from Book b where b.author.name = :name", [name: 'Smith'] ) // Detached criteria (reusable) def criteria = Book.where { pages > 100 } criteria.list() criteria.count() ``` ### Transactions ```groovy // In services (default transactional) @Transactional class BookService { def saveBook(Book book) { book.save(failOnError: true) } } // Manual transaction control Book.withTransaction { status -> def book = new Book(title: "Test") book.save() if (someCondition) { status.setRollbackOnly() } } ``` ## Controllers ### Basic Controller ```groovy class BookController { BookService bookService static allowedMethods = [save: 'POST', update: 'PUT', delete: 'DELETE'] def index() { respond Book.list(), model: [bookCount: Book.count()] } def show(Long id) { respond Book.get(id) } def create() { respond new Book(params) } def save(Book book) { if (book.hasErrors()) { respond book.errors, view: 'create' return } bookService.save(book) redirect action: 'show', id: book.id } def edit(Long id) { respond Book.get(id) } def update(Book book) { if (book.hasErrors()) { respond book.errors, view: 'edit' return } bookService.save(book) redirect action: 'show', id: book.id } def delete(Long id) { bookService.delete(id) redirect action: 'index' } } ``` ### REST Controller ```groovy import grails.rest.RestfulController class BookController extends RestfulController { static responseFormats = ['json', 'xml'] BookController() { super(Book) } // Override to customize @Override protected Book queryForResource(Serializable id) { Book.where { id == id && active == true }.find() } } ``` ### Command Objects ```groovy class BookCommand implements Validateable { String title String author Integer pages static constraints = { title blank: false author blank: false pages min: 1, nullable: true } } class BookController { def save(BookCommand cmd) { if (cmd.hasErrors()) { respond cmd.errors return } // Process valid command } } ``` ### Content Negotiation ```groovy class BookController { def show(Long id) { def book = Book.get(id) respond book // Auto-negotiates based on Accept header } } // Or explicit def show(Long id) { def book = Book.get(id) withFormat { html { render view: 'show', model: [book: book] } json { render book as JSON } xml { render book as XML } } } ``` ## Services ### Basic Service ```groovy import grails.gorm.transactions.Transactional @Transactional class BookService { def findByTitle(String title) { Book.findByTitle(title) } def save(Book book) { book.save(failOnError: true) } @Transactional(readOnly = true) def list(Map params) { Book.list(params) } def delete(Long id) { Book.get(id)?.delete() } } ``` ### Service with Events ```groovy import grails.events.annotation.Publisher import grails.events.annotation.Subscriber class BookService { @Publisher def save(Book book) { book.save() // Automatically publishes 'book.saved' event } } class NotificationService { @Subscriber('book.saved') def onBookSaved(Book book) { // Handle event log.info "Book saved: ${book.title}" } } ``` ### Async Service ```groovy import grails.async.Promise import static grails.async.Promises.* class BookService { Promise findAsync(Long id) { task { Book.get(id) } } def processBooks() { def p1 = task { Book.findAllByActive(true) } def p2 = task { Author.list() } waitAll(p1, p2) // Both complete } } ``` ## Views ### GSP (Groovy Server Pages) ```html Books

Books (${bookCount})

${book.title}

By ${book.author}

View

No books found.

``` ### Common GSP Tags ```html View ... ... ... ${i}: ${item.name} Welcome, ${sec.username()} Login ``` ### JSON Views ```groovy // grails-app/views/book/show.gson import myapp.Book model { Book book } json { id book.id title book.title author book.author datePublished book.datePublished?.format('yyyy-MM-dd') _links { self { href "/books/${book.id}" } } } ``` ### Layouts ```html <g:layoutTitle default="My App"/>
Books ``` ## Interceptors ```groovy class AuthInterceptor { AuthInterceptor() { matchAll() .excludes(controller: 'login') .excludes(controller: 'error') } boolean before() { if (!session.user) { redirect(controller: 'login') return false } true } boolean after() { // After action executes true } void afterView() { // After view renders } } ``` ## Tag Libraries ```groovy class BookTagLib { static namespace = 'book' static defaultEncodeAs = [taglib: 'html'] def formatTitle = { attrs, body -> def title = attrs.title ?: body() out << "${title}" } def ifHasBooks = { attrs, body -> if (Book.count() > 0) { out << body() } } } // Usage in GSP

We have books!

``` ## Configuration ### application.yml ```yaml grails: profile: web codegen: defaultPackage: myapp gorm: reactor: events: false mime: types: json: ['application/json', 'text/json'] xml: ['text/xml', 'application/xml'] environments: development: dataSource: dbCreate: create-drop url: jdbc:h2:mem:devDb test: dataSource: dbCreate: update url: jdbc:h2:mem:testDb production: dataSource: dbCreate: none url: jdbc:postgresql://localhost/myapp driverClassName: org.postgresql.Driver dialect: org.hibernate.dialect.PostgreSQLDialect properties: jmxEnabled: true initialSize: 5 maxActive: 50 server: port: 8080 servlet: context-path: /myapp ``` ### URL Mappings ```groovy // grails-app/controllers/myapp/UrlMappings.groovy class UrlMappings { static mappings = { // Default "/$controller/$action?/$id?(.$format)?" { constraints { // apply constraints here } } // REST resources "/api/books"(resources: 'book') // Custom mappings "/books/$id"(controller: 'book', action: 'show') "/search"(controller: 'book', action: 'search') // Named URL mappings name bookShow: "/books/$id" { controller = 'book' action = 'show' } // Error pages "500"(view: '/error') "404"(view: '/notFound') } } ``` ## Testing with Spock ### Unit Test - Controller ```groovy import grails.testing.web.controllers.ControllerUnitTest import spock.lang.Specification class BookControllerSpec extends Specification implements ControllerUnitTest { def setup() { controller.bookService = Mock(BookService) } void "index returns list of books"() { given: def books = [new Book(title: "Test")] controller.bookService.list(_) >> books when: controller.index() then: model.bookList == books view == '/book/index' } void "show returns 404 for missing book"() { when: controller.show(999) then: response.status == 404 } } ``` ### Unit Test - Service ```groovy import grails.testing.services.ServiceUnitTest import spock.lang.Specification class BookServiceSpec extends Specification implements ServiceUnitTest { void "save persists book"() { given: def book = new Book(title: "Test", author: "Author") when: def result = service.save(book) then: result.id != null Book.count() == 1 } } ``` ### Unit Test - Domain ```groovy import grails.testing.gorm.DomainUnitTest import spock.lang.Specification class BookSpec extends Specification implements DomainUnitTest { void "title cannot be blank"() { when: domain.title = "" domain.author = "Test" then: !domain.validate(['title']) domain.errors['title'].code == 'blank' } void "valid book passes validation"() { when: domain.title = "Valid Title" domain.author = "Author" then: domain.validate() } } ``` ### Integration Test ```groovy import grails.testing.mixin.integration.Integration import grails.gorm.transactions.Rollback import spock.lang.Specification @Integration @Rollback class BookServiceIntegrationSpec extends Specification { BookService bookService void "save and retrieve book"() { given: def book = new Book(title: "Integration Test", author: "Tester") when: bookService.save(book) def found = Book.findByTitle("Integration Test") then: found != null found.author == "Tester" } } ``` ### Functional Test with Geb ```groovy import geb.spock.ContainerGebSpec import grails.testing.mixin.integration.Integration @Integration class BookFunctionalSpec extends ContainerGebSpec { void "can view book list"() { when: go '/book/index' then: title == 'Book List' $('h1').text() == 'Books' } void "can create new book"() { when: go '/book/create' $('form').title = 'New Book' $('form').author = 'Test Author' $('input[type=submit]').click() then: $('h1').text().contains('New Book') } } ``` ## Plugins ### Common Plugins ```groovy // build.gradle // Spring Security implementation 'org.apache.grails.plugins:grails-spring-security-core:7.0.1' // Database Migration (Liquibase) implementation 'org.apache.grails.plugins:grails-database-migration:5.0.1' // Caching implementation 'org.apache.grails.plugins:grails-cache:7.0.0' // Async support implementation 'org.apache.grails.plugins:grails-async:7.0.0' // Fields plugin for form rendering implementation 'org.apache.grails.plugins:grails-fields:7.0.0' // Asset Pipeline runtimeOnly 'cloud.wondrify:asset-pipeline-grails' ``` ### Spring Security Configuration ```groovy // grails-app/conf/application.groovy grails.plugin.springsecurity.userLookup.userDomainClassName = 'myapp.User' grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'myapp.UserRole' grails.plugin.springsecurity.authority.className = 'myapp.Role' grails.plugin.springsecurity.controllerAnnotations.staticRules = [ [pattern: '/', access: ['permitAll']], [pattern: '/error', access: ['permitAll']], [pattern: '/index', access: ['permitAll']], [pattern: '/admin/**', access: ['ROLE_ADMIN']], [pattern: '/api/**', access: ['isAuthenticated()']], ] ``` ## Build Commands ```bash # Run application ./gradlew bootRun # Run tests ./gradlew test # All tests ./gradlew test --tests "BookServiceSpec" # Single spec ./gradlew test --tests "*Spec.save*" # Pattern match ./gradlew integrationTest # Integration tests # Build ./gradlew build # Build with tests ./gradlew bootJar # Executable JAR ./gradlew bootWar # WAR file # Code quality ./gradlew check # Run all verification tasks # Clean ./gradlew clean ``` ## Best Practices ### Convention Over Configuration - Follow Grails naming conventions for automatic wiring. - Use standard directory structure. - Let Grails infer configurations where possible. ### Performance - Use `@GrailsCompileStatic` on controllers, services, and domain classes. - Enable query caching for read-heavy operations. - Use pagination for large result sets. - Avoid N+1 queries with eager fetching or batch size. ### Security - Use CSRF tokens in forms: ``. - Apply Spring Security for authentication/authorization. - Use parameterized queries (GORM handles this automatically). - Encode output to prevent XSS. ### Testing - Write unit tests for domain constraints. - Test controllers with `ControllerUnitTest`. - Use integration tests for service/database interactions. - Use Geb for critical user workflows. ### Code Style - Use `@GrailsCompileStatic` or `@CompileStatic` for type safety and performance. - Prefer services for business logic over controllers. - Use command objects for complex form handling. - Keep controllers thin, services fat. ## Resources - **Grails User Guide**: https://grails.apache.org/docs/latest/guide/single.html - **GORM Documentation**: https://grails.apache.org/docs/latest/grails-data/ - **Grails Plugins**: https://grails.apache.org/plugins.html - **Groovy 5 Documentation**: https://groovy-lang.org/documentation.html#all-versions (select latest 5.1.x) - **Spock Framework**: https://spockframework.org/spock/docs/2.4/all_in_one.html - **Geb Manual**: https://groovy.apache.org/geb/manual/current/