Controllers
Controllers are JavaScript files that contain a set of methods, called actions, reached by the client according to the requestedroute. Whenever a client requests the route, the action performs the business logic code and sends back theresponse. Controllers represent the C in the model-view-controller (MVC) pattern.
In most cases, the controllers will contain the bulk of a project's business logic. But as a controller's logic becomes more and more complicated, it's a good practice to useservices to organize the code into re-usable parts.

Implementation
Controllers can begenerated or added manually. Strapi provides acreateCoreController
factory function that automatically generates core controllers and allows building custom ones orextend or replace the generated controllers.
Adding a new controller
A new controller can be implemented:
- with theinteractive CLI command
strapi generate
- or manually by creating a JavaScript file:
- in
./src/api/[api-name]/controllers/
for API controllers (this location matters as controllers are auto-loaded by Strapi from there) - or in a folder like
./src/plugins/[plugin-name]/server/controllers/
for plugin controllers, though they can be created elsewhere as long as the plugin interface is properly exported in thestrapi-server.js
file (seeServer API for Plugins documentation)
- in
- JavaScript
- TypeScript
const{ createCoreController}=require('@strapi/strapi').factories;
module.exports=createCoreController('api::restaurant.restaurant',({ strapi})=>({
// Method 1: Creating an entirely custom action
asyncexampleAction(ctx){
try{
ctx.body='ok';
}catch(err){
ctx.body= err;
}
},
// Method 2: Wrapping a core action (leaves core logic in place)
asyncfind(ctx){
// some custom logic here
ctx.query={...ctx.query,local:'en'}
// Calling the default core action
const{ data, meta}=awaitsuper.find(ctx);
// some more custom logic
meta.date=Date.now()
return{ data, meta};
},
// Method 3: Replacing a core action with proper sanitization
asyncfind(ctx){
// validateQuery (optional)
// to throw an error on query params that are invalid or the user does not have access to
awaitthis.validateQuery(ctx);
// sanitizeQuery to remove any query params that are invalid or the user does not have access to
// It is strongly recommended to use sanitizeQuery even if validateQuery is used
const sanitizedQueryParams=awaitthis.sanitizeQuery(ctx);
const{ results, pagination}=await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams);
const sanitizedResults=awaitthis.sanitizeOutput(results, ctx);
returnthis.transformResponse(sanitizedResults,{ pagination});
}
}));
import{ factories}from'@strapi/strapi';
exportdefault factories.createCoreController('api::restaurant.restaurant',({ strapi})=>({
// Method 1: Creating an entirely custom action
asyncexampleAction(ctx){
try{
ctx.body='ok';
}catch(err){
ctx.body= err;
}
},
// Method 2: Wrapping a core action (leaves core logic in place)
asyncfind(ctx){
// some custom logic here
ctx.query={...ctx.query,local:'en'}
// Calling the default core action
const{ data, meta}=awaitsuper.find(ctx);
// some more custom logic
meta.date=Date.now()
return{ data, meta};
},
// Method 3: Replacing a core action with proper sanitization
asyncfind(ctx){
// validateQuery (optional)
// to throw an error on query params that are invalid or the user does not have access to
awaitthis.validateQuery(ctx);
// sanitizeQuery to remove any query params that are invalid or the user does not have access to
// It is strongly recommended to use sanitizeQuery even if validateQuery is used
const sanitizedQueryParams=awaitthis.sanitizeQuery(ctx);
const{ results, pagination}=await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams);
// sanitizeOutput to ensure the user does not receive any data they do not have access to
const sanitizedResults=awaitthis.sanitizeOutput(results, ctx);
returnthis.transformResponse(sanitizedResults,{ pagination});
}
}));
Each controller action can be anasync
orsync
function.Every action receives a context object (ctx
) as a parameter.ctx
contains therequest context and theresponse context.
Example: GET /hello route calling a basic controller
A specificGET /hello
route is defined, the name of the router file (i.e.index
) is used to call the controller handler (i.e.index
). Every time aGET /hello
request is sent to the server, Strapi calls theindex
action in thehello.js
controller, which returnsHello World!
:
- JavaScript
- TypeScript
module.exports={
routes:[
{
method:'GET',
path:'/hello',
handler:'hello.index',
}
]
}
module.exports={
asyncindex(ctx, next){// called by GET /hello
ctx.body='Hello World!';// we could also send a JSON
},
};
exportdefault{
routes:[
{
method:'GET',
path:'/hello',
handler:'hello.index',
}
]
}
exportdefault{
asyncindex(ctx, next){// called by GET /hello
ctx.body='Hello World!';// we could also send a JSON
},
};
When a newcontent-type is created, Strapi builds a generic controller with placeholder code, ready to be customized.
To see a possible advanced usage for custom controllers, read theservices and controllers page of the backend customization examples cookbook.
Sanitization and Validation in controllers
It's strongly recommended you sanitize (v4.8.0+) and/or validate (v4.13.0+) your incoming request query utilizing the newsanitizeQuery
andvalidateQuery
functions to prevent the leaking of private data.
Sanitization means that the object is “cleaned” and returned.
Validation means an assertion is made that the data is already clean and throws an error if something is found that shouldn't be there.
In Strapi 5, both query parameters and input data (i.e., create and update body data) are validated. Any create and update data requests with the following invalid input will throw a400 Bad Request
error:
- relations the user do not have permission to create
- unrecognized values that are not present on a schema
- non-writable fields and internal timestamps like
createdAt
andcreatedBy
fields - setting or updating an
id
field (except for connecting relations)
Sanitization when utilizing controller factories
Within the Strapi factories the following functions are exposed that can be used for sanitization and validation:
Function Name | Parameters | Description |
---|---|---|
sanitizeQuery | ctx | Sanitizes the request query |
sanitizeOutput | entity /entities ,ctx | Sanitizes the output data where entity/entities should be an object or array of data |
sanitizeInput | data ,ctx | Sanitizes the input data |
validateQuery | ctx | Validates the request query (throws an error on invalid params) |
validateInput | data ,ctx | (EXPERIMENTAL) Validates the input data (throws an error on invalid data) |
These functions automatically inherit the sanitization settings from the model and sanitize the data accordingly based on the content-type schema and any of the content API authentication strategies, such as the Users & Permissions plugin or API tokens.
Because these methods use the model associated with the current controller, if you query data that is from another model (i.e., doing a find for "menus" within a "restaurant" controller method), you must instead use the@strapi/utils
tools, such assanitize.contentAPI.query
described inSanitizing Custom Controllers, or else the result of your query will be sanitized against the wrong model.
- JavaScript
- TypeScript
const{ createCoreController}=require('@strapi/strapi').factories;
module.exports=createCoreController('api::restaurant.restaurant',({ strapi})=>({
asyncfind(ctx){
awaitthis.validateQuery(ctx);
const sanitizedQueryParams=awaitthis.sanitizeQuery(ctx);
const{ results, pagination}=await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams);
const sanitizedResults=awaitthis.sanitizeOutput(results, ctx);
returnthis.transformResponse(sanitizedResults,{ pagination});
}
}));
import{ factories}from'@strapi/strapi';
exportdefault factories.createCoreController('api::restaurant.restaurant',({ strapi})=>({
asyncfind(ctx){
const sanitizedQueryParams=awaitthis.sanitizeQuery(ctx);
const{ results, pagination}=await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams);
const sanitizedResults=awaitthis.sanitizeOutput(results, ctx);
returnthis.transformResponse(sanitizedResults,{ pagination});
}
}));
Sanitization and validation when building custom controllers
Within custom controllers, there are 5 primary functions exposed via the@strapi/utils
package that can be used for sanitization and validation:
Function Name | Parameters | Description |
---|---|---|
sanitize.contentAPI.input | data ,schema ,auth | Sanitizes the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins |
sanitize.contentAPI.output | data ,schema ,auth | Sanitizes the response output including restricted relations, private fields, passwords, and other nested "visitors" added by plugins |
sanitize.contentAPI.query | ctx.query ,schema ,auth | Sanitizes the request query including filters, sort, fields, and populate |
validate.contentAPI.query | ctx.query ,schema ,auth | Validates the request query including filters, sort, fields (currently not populate) |
validate.contentAPI.input | data ,schema ,auth | (EXPERIMENTAL) Validates the request input including non-writable fields, removing restricted relations, and other nested "visitors" added by plugins |
Depending on the complexity of your custom controllers, you may need additional sanitization that Strapi cannot currently account for, especially when combining the data from multiple sources.
- JavaScript
- TypeScript
const{ sanitize, validate}=require('@strapi/utils');
module.exports={
asyncfindCustom(ctx){
const contentType= strapi.contentType('api::test.test');
await validate.contentAPI.query(ctx.query, contentType,{auth: ctx.state.auth});
const sanitizedQueryParams=await sanitize.contentAPI.query(ctx.query, contentType,{auth: ctx.state.auth});
const documents=await strapi.documents(contentType.uid).findMany(sanitizedQueryParams);
returnawait sanitize.contentAPI.output(documents, contentType,{auth: ctx.state.auth});
}
}
import{ sanitize, validate}from'@strapi/utils';
exportdefault{
asyncfindCustom(ctx){
const contentType= strapi.contentType('api::test.test');
await validate.contentAPI.query(ctx.query, contentType,{auth: ctx.state.auth});
const sanitizedQueryParams=await sanitize.contentAPI.query(ctx.query, contentType,{auth: ctx.state.auth});
const documents=await strapi.documents(contentType.uid).findMany(sanitizedQueryParams);
returnawait sanitize.contentAPI.output(documents, contentType,{auth: ctx.state.auth});
}
}
Extending core controllers
Default controllers and actions are created for each content-type. These default controllers are used to return responses to API requests (e.g. whenGET /api/articles/3
is accessed, thefindOne
action of the default controller for the "Article" content-type is called). Default controllers can be customized to implement your own logic. The following code examples should help you get started.
An action from a core controller can be replaced entirely bycreating a custom action and naming the action the same as the original action (e.g.find
,findOne
,create
,update
, ordelete
).
When extending a core controller, you do not need to re-implement any sanitization as it will already be handled by the core controller you are extending. Where possible it's strongly recommended to extend the core controller instead of creating a custom controller.
Collection type examples
Thebackend customization examples cookbook shows how you can overwrite a default controller action, for instance for thecreate
action.
- `find()`
- findOne()
- create()
- update()
- delete()
asyncfind(ctx){
// some logic here
const{ data, meta}=awaitsuper.find(ctx);
// some more logic
return{ data, meta};
}
asyncfindOne(ctx){
// some logic here
const response=awaitsuper.findOne(ctx);
// some more logic
return response;
}
asynccreate(ctx){
// some logic here
const response=awaitsuper.create(ctx);
// some more logic
return response;
}
asyncupdate(ctx){
// some logic here
const response=awaitsuper.update(ctx);
// some more logic
return response;
}
asyncdelete(ctx){
// some logic here
const response=awaitsuper.delete(ctx);
// some more logic
return response;
}
Single type examples
- find()
- update()
- delete()
asyncfind(ctx){
// some logic here
const response=awaitsuper.find(ctx);
// some more logic
return response;
}
asyncupdate(ctx){
// some logic here
const response=awaitsuper.update(ctx);
// some more logic
return response;
}
asyncdelete(ctx){
// some logic here
const response=awaitsuper.delete(ctx);
// some more logic
return response;
}
Usage
Controllers are declared and attached to a route. Controllers are automatically called when the route is called, so controllers usually do not need to be called explicitly. However,services can call controllers, and in this case the following syntax should be used:
// access an API controller
strapi.controller('api::api-name.controller-name');
// access a plugin controller
strapi.controller('plugin::plugin-name.controller-name');
To list all the available controllers, runyarn strapi controllers:list
.