Girouette

RESTful Resources

Automatic CRUD route generation with the Resource decorator

RESTful Resources

The @Resource decorator automatically generates conventional RESTful routes for your controllers, eliminating boilerplate and ensuring consistency.

Basic Resource

import { Resource } from '@adonisjs-community/girouette'
import { HttpContext } from '@adonisjs/core/http'

@Resource('posts')
export default class PostsController {
  async index() {
    // GET /posts
    return Post.all()
  }

  async create() {
    // GET /posts/create
    return 'Show create form'
  }

  async store({ request }: HttpContext) {
    // POST /posts
    return Post.create(request.body())
  }

  async show({ params }: HttpContext) {
    // GET /posts/:id
    return Post.findOrFail(params.id)
  }

  async edit({ params }: HttpContext) {
    // GET /posts/:id/edit
    return `Edit form for post ${params.id}`
  }

  async update({ params, request }: HttpContext) {
    // PUT/PATCH /posts/:id
    const post = await Post.findOrFail(params.id)
    return post.merge(request.body()).save()
  }

  async destroy({ params }: HttpContext) {
    // DELETE /posts/:id
    const post = await Post.findOrFail(params.id)
    await post.delete()
    return { deleted: true }
  }
}

Generated Routes

The @Resource('posts') decorator creates these routes:

MethodURLController MethodRoute Name
GET/postsindexposts.index
GET/posts/createcreateposts.create
POST/postsstoreposts.store
GET/posts/:idshowposts.show
GET/posts/:id/editeditposts.edit
PUT/PATCH/posts/:idupdateposts.update
DELETE/posts/:iddestroyposts.destroy

You only need to implement the actions you need. Girouette won't create routes for missing methods.

Custom Parameter Names

Rename the route parameter from :id to something more meaningful:

import { Resource } from '@adonisjs-community/girouette'

@Resource({ name: 'articles', params: { articles: 'slug' } })
export default class ArticlesController {
  async show({ params }: HttpContext) {
    // GET /articles/:slug
    // params.slug instead of params.id
    return Article.findByOrFail('slug', params.slug)
  }

  async update({ params, request }: HttpContext) {
    // PUT/PATCH /articles/:slug
    const article = await Article.findByOrFail('slug', params.slug)
    return article.merge(request.body()).save()
  }
}

Nested Resources

Create nested resources for parent-child relationships:

import { Resource } from '@adonisjs-community/girouette'

@Resource({ name: 'users.posts', params: { users: 'userId', posts: 'postId' } })
export default class UserPostsController {
  async index({ params }: HttpContext) {
    // GET /users/:userId/posts
    const user = await User.findOrFail(params.userId)
    return user.related('posts').query()
  }

  async show({ params }: HttpContext) {
    // GET /users/:userId/posts/:postId
    return Post.query()
      .where('userId', params.userId)
      .where('id', params.postId)
      .firstOrFail()
  }

  async store({ params, request }: HttpContext) {
    // POST /users/:userId/posts
    const user = await User.findOrFail(params.userId)
    return user.related('posts').create(request.body())
  }

  async destroy({ params }: HttpContext) {
    // DELETE /users/:userId/posts/:postId
    const post = await Post.query()
      .where('userId', params.userId)
      .where('id', params.postId)
      .firstOrFail()
    await post.delete()
    return { deleted: true }
  }
}

Deeply Nested Resources

@Resource({
  name: 'teams.projects.tasks',
  params: { teams: 'teamId', projects: 'projectId', tasks: 'taskId' }
})
export default class TasksController {
  async show({ params }: HttpContext) {
    // GET /teams/:teamId/projects/:projectId/tasks/:taskId
    return Task.query()
      .where('projectId', params.projectId)
      .where('id', params.taskId)
      .firstOrFail()
  }
}

Filtering Resource Actions

@Pick - Include Only Specific Actions

import { Resource, Pick } from '@adonisjs-community/girouette'

@Resource('products')
@Pick(['index', 'show'])
export default class ProductsController {
  async index() {
    // GET /products - included
    return Product.all()
  }

  async show({ params }: HttpContext) {
    // GET /products/:id - included
    return Product.findOrFail(params.id)
  }

  // create, store, edit, update, destroy are NOT registered
}

@Except - Exclude Specific Actions

import { Resource, Except } from '@adonisjs-community/girouette'

@Resource('articles')
@Except(['create', 'edit'])
export default class ArticlesController {
  async index() { /* ... */ }    // Included
  async store() { /* ... */ }    // Included
  async show() { /* ... */ }     // Included
  async update() { /* ... */ }   // Included
  async destroy() { /* ... */ }  // Included
  // create and edit are excluded
}

@ApiOnly - API Resources

For JSON APIs, exclude form-rendering actions (create and edit):

import { Resource, ApiOnly } from '@adonisjs-community/girouette'

@Resource('api.users')
@ApiOnly()
export default class ApiUsersController {
  async index() {
    // GET /api/users
    return User.all()
  }

  async store({ request }: HttpContext) {
    // POST /api/users
    return User.create(request.body())
  }

  async show({ params }: HttpContext) {
    // GET /api/users/:id
    return User.findOrFail(params.id)
  }

  async update({ params, request }: HttpContext) {
    // PUT/PATCH /api/users/:id
    const user = await User.findOrFail(params.id)
    return user.merge(request.body()).save()
  }

  async destroy({ params }: HttpContext) {
    // DELETE /api/users/:id
    const user = await User.findOrFail(params.id)
    await user.delete()
    return { deleted: true }
  }

  // 'create' and 'edit' actions are automatically excluded
}

@ApiOnly() is equivalent to @Except(['create', 'edit']). Use it for APIs that don't render HTML forms.

Resource Middleware

Apply middleware to specific resource actions:

import { Resource, ResourceMiddleware } from '@adonisjs-community/girouette'
import { middleware } from '#start/kernel'

@Resource('posts')
@ResourceMiddleware(['store', 'update', 'destroy'], [middleware.auth()])
export default class PostsController {
  async index() {
    // Public
  }

  async show() {
    // Public
  }

  async store() {
    // Protected
  }

  async update() {
    // Protected
  }

  async destroy() {
    // Protected
  }
}

Multiple Middleware Configurations

@Resource('comments')
@ResourceMiddleware(['index', 'show'], [middleware.cache(60)])
@ResourceMiddleware(['store'], [middleware.auth(), middleware.throttle()])
@ResourceMiddleware(['update', 'destroy'], [middleware.auth(), middleware.ownership()])
export default class CommentsController {
  // Different middleware for different action groups
}

Combining with Groups

Resources work seamlessly with group decorators:

import { Group, GroupMiddleware, Resource, ApiOnly } from '@adonisjs-community/girouette'
import { middleware } from '#start/kernel'

@Group({ name: 'api.v1', prefix: '/api/v1' })
@GroupMiddleware([middleware.auth('api')])
@Resource('projects')
@ApiOnly()
export default class ProjectsController {
  async index() {
    // GET /api/v1/projects
    // Route name: api.v1.projects.index
    // Protected by API auth
  }

  async store({ request, auth }: HttpContext) {
    // POST /api/v1/projects
    return auth.user!.related('projects').create(request.body())
  }
}

Resource Comparison

DecoratorActions Included
@Resource('name')All 7 actions
@Resource('name') @ApiOnly()index, show, store, update, destroy
@Resource('name') @Pick(['index', 'show'])Only index and show
@Resource('name') @Except(['destroy'])All except destroy

Next Steps

On this page