Overview
The Controller class is responsible for taking in requests from the outside world and returning the appropriate response.
Think of a Controller as a server at a restaurant. A client makes a request to an application, that request is routed to the appropriate Controller and then the Controller interprets the request and returns data relative to what the client has request.
Actions
Controller actions are functions that call on a Controller in response to an incoming HTTP request. The job of Controller actions are to return the data that the Lux Application will respond with.
There is no special API for Controller actions. They are simply functions that return a value. If an action returns a Query or Promise the resolved value will be used rather than the immediate return value of the action.
Below you will find a table showing the different types of responses you can get from different action return values. Keep in mind, Lux is agnostic to whether or not the value is returned synchronously or resolved from a Promise.
Return/Resolved Value | Response |
---|---|
Array | Serialized JSON String |
Array or Object Literal | JSON String |
String Literal | Plain Text |
Number Literal | HTTP Status Code |
true | 204 No Content |
false | 401 Unauthorized |
Built-In Actions
Built-in actions refer to Controller actions that you get for free when extending the Controller class (show, index, create, update, destroy). These actions are highly optimized to load only the attributes and relationships that are defined in the resolved Serializer for a Controller.
If applicable, built-in actions support the following features described in the JSON API specification:
Extending Built-In Actions
Considering the amount of functionality built-in actions provide, you will rarely need to override the default behavior of a built-in action. In the event that you do need to override a built-in action, you have the ability to opt back into the built-in logic by calling the super class.
Read actions such as index and show return a Query which allows us to chain methods to the super call. In the following example we will extend the default behavior of the index action to only match records that meet an additional hard-coded set of conditions. We will still be able to use all of the functionality that the built-in index action provides.
// app/controllers/posts.js
import { Controller } from 'lux-framework';
class PostsController extends Controller {
index(request, response) {
return super.index(request, response).where({
isPublic: true
});
}
}
export default PostsController;
Custom Actions
Sometimes it is necessary to add a custom action to a Controller. Lux allows you to do so by adding an instance method to a Controller. In the following example you will see how to add a custom action with the name check
to a Controller. We are implementing this action to use as a health check for the application so we want to return the Number
literal 204
.
// app/controllers/health.js
import { Controller } from 'lux-framework';
class HealthController extends Controller {
async check() {
return 204;
}
}
export default HealthController;
The example above is nice but we can make the code a bit more concise with an Arrow Function
.
// app/controllers/health.js
import { Controller } from 'lux-framework';
class HealthController extends Controller {
check = async () => 204;
}
export default HealthController;
Using an Arrow Function instead of a traditional method Controller can be useful when immediately returning a value. However, there are a few downsides to using an Arrow Function
for a Controller action, such as not being able to call the super class
. This can be an issue if you are looking to extend a built-in action.
Another use case for a custom action could be to return a specific scope of data from a Model
. Let's implement a custom drafts
route on a PostsController
.
// app/controllers/posts.js
import { Controller } from 'lux-framework';
import Post from 'app/models/posts';
class PostsController extends Controller {
drafts() {
return Post.where({
isPublic: false
});
}
}
export default PostsController;
While the example above works, we would have to implement all the custom logic that we get for free with built-in actions. Since we aren't getting too crazy with our custom action we can likely just call the index
action and chain a .where()
to it.
// app/controllers/posts.js
import { Controller } from 'lux-framework';
class PostsController extends Controller {
drafts(request, response) {
return this.index(request, response).where({
isPublic: false
});
}
}
export default PostsController;
Now we can sort, filter, and paginate our custom drafts
route!
Middleware
Middleware can be a very powerful tool in many Node.js server frameworks. Lux is no exception. Middleware can be used to execute logic before or after a Controller action is executed.
There are two hooks where you can execute middleware functions, beforeAction
and afterAction
. Functions added to the beforeAction
hook will execute before the Controller action and functions added to the afterAction
hook will be executed after the Controller
action.
Context
Middleware functions will be bound to the Controller they are added to upon the start of an Application.
Due to the lexical binding of arrow functions, if you need to use the this
keyword within a middleware function, declare the middleware function using the function
keyword and not as an arrow function.
Scoping Middleware
Middleware is scoped by Controller and includes a parent Controller's middleware recursively until the parent Controller is the root ApplicationController
. This allows you to implement custom logic that can be executed for resources, namespaces, or an entire Application.
Let's say we want to require authentication for every route in our Application. All we have to do is move our authentication middleware function from the example above to the ApplicationController
.
// app/controllers/application.js
import { Controller } from 'lux-framework';
class ApplicationController extends Controller {
beforeAction = [
async function authenticate(request) {
if (!request.currentUser) {
// 401 Unauthorized
return false;
}
}
];
}
export default ApplicationController;
Execuation Order
Understanding the execution order of middleware functions and a Controller
action is essential to productivity with Lux. Depending on what you use case is, you may want your function to execute at different times in the request
/ response
cycle.
- Parent
Controller
beforeAction
hooks Controller
beforeAction
hooksController
ActionController
afterAction
hooks- Parent
Controller
afterAction
hooks
Modules
It is considered a best practice to define your middleware functions in separate file and export them for use throughout an Application. Typically this is done within an app/middleware
directory.
// app/middleware/authenticate.js
export default async function authenticate(request) {
if (!request.currentUser) {
// 401 Unauthorized
return false;
}
}
This keeps the Controller code clean, easier to read, and easier to modify.
// app/controllers/application.js
import { Controller } from 'lux-framework';
import authenticate from 'app/middleware/authenticate';
class ApplicationController extends Controller {
beforeAction = [
authenticate
];
}
export default ApplicationController;
Properties
- Source:
src/packages/controller/index.js:402
Functions to execute on each request handled by a
Controller
after theController
action is executed.Functions called from the
afterAction
hook will haverequest
andresponse
objects passed as arguments as well as a thirdpayload
argument. Thepayload
argument is a reference to the resolved data of the Controller action that was called within the currentrequest
/response
cycle. You need to explicitly return thispayload
in order for the afterAction to resolve with it's data. If you return a modified value from a function added to theafterAction
hook, that value will be used instead of the resolved data from the preceding Controller action. Subsequent hooks called from anafterAction
hook will will use the value returned or resolved from the preceding hook. This makesafterAction
a great place to modify the data you are sending back to the client.Example:
import { Controller } from 'lux-framework'; async function addCopyright(request, response, payload) { const { action } = request; if (payload && action !== preflight) { return { ...payload, meta: { copyright: '2016 (c) Postlight' } }; } return payload; } class ApplicationController extends Controller { afterAction = [ addCopyright ]; } export default ApplicationController;
- Source:
src/packages/controller/index.js:348
Functions to execute on each request handled by a
Controller
before theController
action is executed.Functions added to the
beforeAction
hook behave similarly toController
actions, however, they are expected to returnundefined
. If a middleware function returns a value other thanundefined
therequest
/response
cycle will end before remaining middleware and/or Controller actions are executed. This makes thebeforeAction
hook a very powerful tool for dealing with many common tasks, such as authentication.Functions called from the
beforeAction
hook will haverequest
andresponse
objects passed as arguments.Example:
import { Controller } from 'lux-framework'; const UNSAFE_METHODS = /(?:POST|PATCH|DELETE)/i; function isAdmin(user) { if (user) { return user.isAdmin; } return false; } async function authentication(request) { const { method, currentUser } = request; const isUnsafe = UNSAFE_METHODS.test(method); if (isUnsafe && !isAdmin(currentUser)) { return false; // 401 Unauthorized if the current user is not an admin. } } class PostsController extends Controller { beforeAction = [ authentication ]; } export default PostsController;
- Source:
src/packages/controller/index.js:454
The default amount of items to include per each response of the index action if a
?page[size]
query parameter is not specified. - Source:
src/packages/controller/index.js:318
An array of filter query parameter keys that are allowed to reach a Controller instance from an incoming
HTTP
request.If you do not override this property all of the attributes specified in the Serializer that represents a Controller's resource. If the Serializer cannot be resolved, this property will default to an empty array.
- Source:
src/packages/controller/index.js:333
An array of parameter keys that are allowed to reach a Controller instance from an incoming
POST
orPATCH
request body.If you do not override this property all of the attributes specified in the Serializer that represents a Controller's resource. If the Serializer cannot be resolved, this property will default to an empty array.
- Source:
src/packages/controller/index.js:278
An array of custom query parameter keys that are allowed to reach a Controller instance from an incoming
HTTP
request.For security reasons, query parameters passed to Controller actions from an incoming request other than sort, filter, and page must have their key whitelisted.
class UsersController extends Controller { // Allow the following custom query parameters to be used for this // Controller's actions. query = [ 'cache' ]; }
- Source:
src/packages/controller/index.js:303
An array of sort query parameter values that are allowed to reach a Controller instance from an incoming
HTTP
request.If you do not override this property all of the attributes specified in the Serializer that represents a Controller's resource. If the Serializer cannot be resolved, this property will default to an empty array.
Methods
- Source:
src/packages/controller/index.js:597
Create and return a single Model instance that the Controller instance represents. For more information, see the creating resources section of the JSON API specification.
- Source:
src/packages/controller/index.js:678
Destroy a single Model instance that the Controller instance represents. For more information, see the deleting resources section of the JSON API specification.
- Source:
src/packages/controller/index.js:565
This method supports filtering, sorting, pagination, including relationships, and sparse fieldsets via query parameters. For more information, see the fetching resources section of the JSON API specification.
- Source:
src/packages/controller/index.js:695
Respond to HEAD or OPTIONS requests.
- Source:
src/packages/controller/index.js:581
This method supports including relationships, and sparse fieldsets via query parameters. For more information, see the fetching resources section of the JSON API specification.
- Source:
src/packages/controller/index.js:638
Update and return a single Model instance that the Controller instance represents. For more information, see the updating resourcessection of the JSON API specification.
Return Value
Resolves with the updated Model if changes occur. Resolves with the number
204
if no changes occur.