Routes#
Routes map incoming URLs to controllers and ship pre-generated for each content type. This documentation shows how to add or customize core and custom routers and attach policies or middlewares for extra control.Requests sent to Strapi on any URL are handled by routes. By default, Strapi generates routes for all the content-types (see REST API documentation). Routes can be added and configured:
- with policies, which are a way to block access to a route,
- and with middlewares, which are a way to control and change the request flow and the request itself.
Once a route exists, reaching it executes some code handled by a controller (see controllers documentation). To view all existing routes and their hierarchal order, you can run yarn strapi routes:list (see CLI reference).
Implementation#
Implementing a new route consists in defining it in a router file within the ./src/api/[apiName]/routes folder (see project structure).
There are 2 different router file structures, depending on the use case:
- configuring core routers
- or creating custom routers.
Configuring core routers#
Core routers (i.e. find, findOne, create, update, and delete) correspond to default routes automatically created by Strapi when a new content-type is created.
Strapi provides a createCoreRouter factory function that automatically generates the core routers and allows:
- passing in configuration options to each router
- and disabling some core routers to create custom ones.
A core router file is a JavaScript file exporting the result of a call to createCoreRouter with the following parameters:
| Parameter | Description | Type |
|---|---|---|
prefix | Allows passing in a custom prefix to add to all routers for this model (e.g. /test) | String |
only | Core routes that will only be loaded Anything not in this array is ignored. | Array |
except | Core routes that should not be loaded This is functionally the opposite of the only parameter. | Array |
config | Configuration to handle policies, middlewares and public availability for the route | Object |
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::restaurant.restaurant', {
prefix: '',
only: ['find', 'findOne'],
except: [],
config: {
find: {
auth: false,
policies: [],
middlewares: [],
},
findOne: {},
create: {},
update: {},
delete: {},
},
});
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::restaurant.restaurant', {
prefix: '',
only: ['find', 'findOne'],
except: [],
config: {
find: {
auth: false,
policies: [],
middlewares: [],
},
findOne: {},
create: {},
update: {},
delete: {},
},
});
Generic implementation example:
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::restaurant.restaurant', {
only: ['find'],
config: {
find: {
auth: false,
policies: [],
middlewares: [],
}
}
});
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::restaurant.restaurant', {
only: ['find'],
config: {
find: {
auth: false,
policies: [],
middlewares: [],
}
}
});
This only allows a GET request on the /restaurants path from the core find controller without authentication. When you reference custom controller actions in custom routers, prefer the fully‑qualified api::<api-name>.<controllerName>.<actionName> form for clarity (e.g., api::restaurant.restaurant.review).
Creating custom routers#
Creating custom routers consists in creating a file that exports an array of objects, each object being a route with the following parameters:
| Parameter | Description | Type |
|---|---|---|
method | Method associated to the route (i.e. GET, POST, PUT, DELETE or PATCH) | String |
path | Path to reach, starting with a forward-leading slash (e.g. /articles) | String |
handler | Function to execute when the route is reached. Use the fully-qualified syntax api::api-name.controllerName.actionName (or plugin::plugin-name.controllerName.actionName). The short <controllerName>.<actionName> form for legacy projects also works. | String |
configOptional | Configuration to handle policies, middlewares and public availability for the route | Object |
Dynamic routes can be created using parameters and regular expressions. These parameters will be exposed in the ctx.params object. For more details, please refer to the documentation.
Routes files are loaded in alphabetical order. To load custom routes before core routes, make sure to name custom routes appropriately (e.g. 01-custom-routes.js and 02-core-routes.js).
:::info Controller handler naming reference
The handler string acts as a pointer to the controller action that should run for the route. Strapi supports the following formats:
- API controllers:
api::<api-name>.<controllerName>.<actionName>(e.g.api::restaurant.restaurant.exampleAction). The<controllerName>comes from the controller filename inside./src/api/<api-name>/controllers/. - Plugin controllers:
plugin::<plugin-name>.<controllerName>.<actionName>when the controller lives in a plugin.
For backwards compatibility, Strapi also accepts a short <controllerName>.<actionName> string for API controllers, but using the fully-qualified form makes the route more explicit and avoids naming collisions across APIs and plugins.
:::
Example of a custom router using URL parameters and regular expressions for routes
In the following example, the custom routes file name is prefixed with 01- to make sure the route is reached before the core routes.
/** @type {import('@strapi/strapi').Core.RouterConfig} */
const config = {
type: 'content-api',
routes: [
{ // Path defined with an URL parameter
method: 'POST',
path: '/restaurants/:id/review',
handler: 'api::restaurant.restaurant.review',
},
{ // Path defined with a regular expression
method: 'GET',
path: '/restaurants/:category([a-z]+)', // Only match when the URL parameter is composed of lowercase letters
handler: 'api::restaurant.restaurant.findByCategory',
}
]
}
module.exports = config
import type { Core } from '@strapi/strapi';
const config: Core.RouterConfig = {
type: 'content-api',
routes: [
{ // Path defined with a URL parameter
method: 'GET',
path: '/restaurants/:category/:id',
handler: 'api::restaurant.restaurant.findOneByCategory',
},
{ // Path defined with a regular expression
method: 'GET',
path: '/restaurants/:region(\\d{2}|\\d{3})/:id', // Only match when the first parameter contains 2 or 3 digits.
handler: 'api::restaurant.restaurant.findOneByRegion',
}
]
}
export default config
Configuration#
Both core routers and custom routers have the same configuration options. The routes configuration is defined in a config object that can be used to handle policies and middlewares or to make the route public.
Policies#
Policies can be added to a route configuration:
- by pointing to a policy registered in
./src/policies, with or without passing a custom configuration - or by declaring the policy implementation directly, as a function that takes
policyContextto extend (ctx) and thestrapiinstance as arguments (see policies documentation)
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::restaurant.restaurant', {
config: {
find: {
policies: [
// point to a registered policy
'policy-name',
// point to a registered policy with some custom configuration
{ name: 'policy-name', config: {} },
// pass a policy implementation directly
(policyContext, config, { strapi }) => {
return true;
},
]
}
}
});
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::restaurant.restaurant', {
config: {
find: {
policies: [
// point to a registered policy
'policy-name',
// point to a registered policy with some custom configuration
{ name: 'policy-name', config: {} },
// pass a policy implementation directly
(policyContext, config, { strapi }) => {
return true;
},
]
}
}
});
module.exports = {
routes: [
{
method: 'GET',
path: '/articles/customRoute',
handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller
config: {
policies: [
// point to a registered policy
'policy-name',
// point to a registered policy with some custom configuration
{ name: 'policy-name', config: {} },
// pass a policy implementation directly
(policyContext, config, { strapi }) => {
return true;
},
]
},
},
],
};
export default {
routes: [
{
method: 'GET',
path: '/articles/customRoute',
handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller
config: {
policies: [
// point to a registered policy
'policy-name',
// point to a registered policy with some custom configuration
{ name: 'policy-name', config: {} },
// pass a policy implementation directly
(policyContext, config, { strapi }) => {
return true;
},
]
},
},
],
};
Middlewares#
Middlewares can be added to a route configuration:
- by pointing to a middleware registered in
./src/middlewares, with or without passing a custom configuration - or by declaring the middleware implementation directly, as a function that takes (
ctx) and thestrapiinstance as arguments:
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::restaurant.restaurant', {
config: {
find: {
middlewares: [
// point to a registered middleware
'middleware-name',
// point to a registered middleware with some custom configuration
{ name: 'middleware-name', config: {} },
// pass a middleware implementation directly
(ctx, next) => {
return next();
},
]
}
}
});
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::restaurant.restaurant', {
config: {
find: {
middlewares: [
// point to a registered middleware
'middleware-name',
// point to a registered middleware with some custom configuration
{ name: 'middleware-name', config: {} },
// pass a middleware implementation directly
(ctx, next) => {
return next();
},
]
}
}
});
module.exports = {
routes: [
{
method: 'GET',
path: '/articles/customRoute',
handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller
config: {
middlewares: [
// point to a registered middleware
'middleware-name',
// point to a registered middleware with some custom configuration
{ name: 'middleware-name', config: {} },
// pass a middleware implementation directly
(ctx, next) => {
return next();
},
],
},
},
],
};
export default {
routes: [
{
method: 'GET',
path: '/articles/customRoute',
handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller
config: {
middlewares: [
// point to a registered middleware
'middleware-name',
// point to a registered middleware with some custom configuration
{ name: 'middleware-name', config: {} },
// pass a middleware implementation directly
(ctx, next) => {
return next();
},
],
},
},
],
};
Public routes#
By default, routes are protected by Strapi's authentication system, which is based on API tokens or on the use of the Users & Permissions plugin.
In some scenarios, it can be useful to have a route publicly available and control the access outside of the normal Strapi authentication system. This can be achieved by setting the auth configuration parameter of a route to false:
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::restaurant.restaurant', {
config: {
find: {
auth: false
}
}
});
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::restaurant.restaurant', {
config: {
find: {
auth: false
}
}
});
module.exports = {
routes: [
{
method: 'GET',
path: '/articles/customRoute',
handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller
config: {
auth: false,
},
},
],
};
export default {
routes: [
{
method: 'GET',
path: '/articles/customRoute',
handler: 'api::api-name.controllerName.functionName', // or 'plugin::plugin-name.controllerName.functionName' for a plugin-specific controller
config: {
auth: false,
},
},
],
};
Custom Content API parameters {#custom-content-api-parameters}#
You can extend the query and body parameters allowed on Content API routes by registering them in the register lifecycle. Registered parameters are then validated and sanitized like core parameters. Clients can send extra query keys (e.g. ?search=...) or root-level body keys (e.g. clientMutationId) without requiring custom routes or controllers.
| What | Where |
|---|---|
| Enable strict parameters (reject unknown query/body keys) | API configuration: set rest.strictParams: true in ./config/api.js (or ./config/api.ts). |
| Add allowed parameters (app) | Call addQueryParams / addInputParams in register in ./src/index.js or ./src/index.ts. |
| Add allowed parameters (plugin) | Call addQueryParams / addInputParams in the plugin's register lifecycle. |
When rest.strictParams is enabled, only core parameters and parameters on each route's request schema are accepted; the parameters you register are merged into that schema. Use the z instance from @strapi/utils (or zod/v4) for schemas.
addQueryParams#
strapi.contentAPI.addQueryParams(options) registers extra query parameters. Schemas must be scalar or array-of-scalars (string, number, boolean, enum). For nested structures, use addInputParams instead. Each entry can have an optional matchRoute: (route) => boolean callback to add the parameter only to routes for which the callback returns true. You cannot register core query param names (e.g. filters, sort, fields) as extra params; they are reserved.
addInputParams#
strapi.contentAPI.addInputParams(options) registers extra input parameters: root-level keys in the request body (e.g. alongside data), with any Zod type. The optional matchRoute callback works the same way as for addQueryParams. You cannot register reserved names such as id or documentId as input params.
matchRoute#
The matchRoute callback receives a route object with the following properties:
route.method: the HTTP method ('GET','POST', etc.)route.path: the route pathroute.handler: the controller action stringroute.info: metadata about the route
For example, to target only GET routes, use matchRoute: (route) => route.method === 'GET'. To target only routes whose path includes articles, use matchRoute: (route) => route.path.includes('articles').
module.exports = {
register({ strapi }) {
strapi.contentAPI.addQueryParams({
search: {
schema: (z) => z.string().max(200).optional(),
matchRoute: (route) => route.path.includes('articles'),
},
});
strapi.contentAPI.addInputParams({
clientMutationId: {
schema: (z) => z.string().max(100).optional(),
},
});
},
};
export default {
register({ strapi }) {
strapi.contentAPI.addQueryParams({
search: {
schema: (z) => z.string().max(200).optional(),
matchRoute: (route) => route.path.includes('articles'),
},
});
strapi.contentAPI.addInputParams({
clientMutationId: {
schema: (z) => z.string().max(100).optional(),
},
});
},
};