Skip to main content

Controllers

Understand how you can set up controllers in your REST API application.

Introduction​

Instead of defining all of your request handling logic as closures in your route files, you may wish to organize this behavior using "controller" classes. Controllers can group related request handling logic into a single class. For example, a UserController class might handle all incoming requests related to users, including showing, creating, updating, and deleting users. By default, controllers are stored in the

Path.controllers()

./src/http/controllers

directory.

Writing controllers​

Let's take a look at an example:

import { Context, Controller } from '@athenna/http'

@Controller()
export class UserController {
public async show({ request, response }: Context) {
return response.send(`User_${request.param('id')}`)
}
}

You can define a route to this controller method like so:

Route.controller('UserController').get('/user/:id', 'show')

// or

Route.get('/user/:id', 'UserController.show')

When an incoming request matches the specified route URL, the show method on the UserController class will be invoked and the route parameters will be passed to the method.

Resource controllers​

If you think of each model in your application as a "resource", it is typical to perform the same sets of actions against each resource in your application. For example, imagine your application contains a Photo model and a Movie model. It is likely that users can create, read, update, or delete these resources.

Because of this common use case, Athenna resource routing assigns the typical creation, read, update, and delete ("CRUD") routes to a controller with a single line of code. To get started, we can use the make:controller command's --resource option to quickly create a controller to handle these actions:

node artisan make:controller PhotoController --resource

This command will generate a controller at

Path.controllers('PhotoController.ts')

./src/http/controllers/PhotoController.ts

. The controller will contain a method for each of the available resource operations. Next, you may register a resource route that points to the controller:

Route.resource('photos', 'PhotoController')

This single route declaration creates multiple routes to handle a variety of actions on the resource. The generated controller will already have methods stubbed for each of these actions. Remember, you can always get a quick overview of your application's routes by running the route:list command.

MethodURLActionRoute name
GET/photosindexPhotoController.index
POST/photosstorePhotoController.store
GET/photos/:idshowPhotoController.show
PUT/photos/:idupdatePhotoController.update
DELETE/photos/:iddeletePhotoController.delete

Partial resource routes​

When declaring a resource route, you may specify a subset of actions the controller should handle instead of the full set of default actions:

Route.resource('photos', 'PhotoController').only(['index', 'show'])

Route.resource('photos', 'PhotoController').except(['store', 'update', 'delete'])

Resource middlewares​

The middleware(), interceptor() and terminator() methods of a resource will register the middleware in all the routes of the resource:

Route.resource('photos', 'PhotoController').middleware('auth')

But it's very common to have actions that are public, like listing the photos, and actions that need authentication, like creating or deleting one. For these cases, you can use the only and except options to choose in which actions the middleware will be registered:

// Only the store, update and delete routes will use auth
Route.resource('photos', 'PhotoController')
.middleware('auth', { only: ['store', 'update', 'delete'] })

// All routes except index and show will use auth
Route.resource('photos', 'PhotoController')
.middleware('auth', { except: ['index', 'show'] })

The same options are available for interceptors and terminators. You can also use the prepend option to register the middleware before the ones that are already registered in the routes:

Route.resource('photos', 'PhotoController')
.interceptor('responseFormatter', { only: ['index'] })
.terminator('auditLog', { except: ['index', 'show'], prepend: true })
warning

In old versions of Athenna the second argument of these methods was a prepend boolean. If you were using .middleware('auth', true), change it to .middleware('auth', { prepend: true }).

Nested resources​

Sometimes you may need to define routes to a nested resource. For example, a photo resource may have multiple comments that may be attached to the photo. To nest the resource controllers, you may use "dot" notation in your route declaration:

Route.resource('photos.comments', 'PhotoCommentController')

This route will register a nested resource that may be accessed with URLs like the following:

/photos/:photo_id/comments
/photos/:photo_id/comments/:id

Dependency injection and controllers​

The Athenna service container is used to resolve all Athenna controllers. As a result, you are able to use any dependencies your controller may need using @Inject() annotation or in its constructor. The declared dependencies will automatically be resolved and injected into the controller instance when receiving a request from the server:

import { Inject } from '@athenna/ioc'
import { Controller, type Context } from '@athenna/http'
import { UserService } from '#src/services/UserService'
import { ProfileService } from '#src/services/ProfileService'

@Controller()
export class UserController {
private userService: UserService

@Inject() 👈
private profileService: ProfileService

public constructor(userService) { 👈
this.userService = userService
}

public async show({ response, params }: Context) {
//
}
}
tip

To learn more about the @Controller() annotation, visit the REST API annotations documentation page.