Server API: Routes
Page summary:
The Server API exports a
routesvalue from the server entry file to expose plugin endpoints. Use the array format only for implicit admin routes, the named router format to separate admin and Content API routes, or the factory callback format for dynamic route configuration.
Routes expose your plugin's HTTP endpoints and map incoming requests to controller actions. They are exported from the server entry file as a routes value.
Before diving deeper into the concepts on this page, please ensure you have:
- created a Strapi plugin,
- read and understood the basics of the Server API
Route declaration formats
- Array format: Simple plugins that only need admin routes with default registration behavior
- Named router format: Plugins that expose both admin and Content API routes, or need explicit type control. Recommended for most cases.
- Factory callback format: Advanced cases where route config depends on the
strapiinstance (e.g., reading plugin configuration).
Array format
The array format is the most basic format: it exports an array of route objects directly. Strapi registers these objects as admin routes by default, with the plugin name as prefix.
To expose Content API routes, use the named router format with type: 'content-api'.
- JavaScript
- TypeScript
'use strict';
module.exports = [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
policies: [],
},
},
{
method: 'POST',
path: '/articles',
handler: 'article.create',
config: {
policies: [],
},
},
];
export default [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
policies: [],
},
},
{
method: 'POST',
path: '/articles',
handler: 'article.create',
config: {
policies: [],
},
},
];
Named router format
With the named router format, use an object with named keys (admin, content-api, or any custom name) to declare separate router groups. Each group is a router object with a type, optional prefix, and a routes array. Use this format when your plugin exposes both admin and Content API routes.
- JavaScript
- TypeScript
'use strict';
const adminRoutes = require('./admin');
const contentApiRoutes = require('./content-api');
module.exports = {
admin: adminRoutes,
'content-api': contentApiRoutes,
};
'use strict';
module.exports = {
type: 'admin',
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
],
};
'use strict';
module.exports = {
type: 'content-api',
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
policies: [],
},
},
],
};
import adminRoutes from './admin';
import contentApiRoutes from './content-api';
export default {
admin: adminRoutes,
'content-api': contentApiRoutes,
};
export default {
type: 'admin' as const,
routes: [
{
method: 'GET' as const,
path: '/articles',
handler: 'article.find',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
],
};
export default {
type: 'content-api' as const,
routes: [
{
method: 'GET' as const,
path: '/articles',
handler: 'article.find',
config: {
policies: [],
},
},
],
};
Factory callback format
For advanced cases where you need access to the strapi instance at route configuration time (for example, to build dynamic paths or conditionally include routes based on configuration), you can export a factory callback.
The factory callback must be attached to a named route entry (such as admin or content-api), not exported as the root of routes/index.
module.exports = ({ strapi }) => ({ ... }) at the root level is not a valid format.
- JavaScript
- TypeScript
'use strict';
module.exports = {
'content-api': ({ strapi }) => ({
type: 'content-api',
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
auth: strapi.plugin('my-plugin').config('publicRead') ? false : {},
},
},
],
}),
};
import type { Core } from '@strapi/strapi';
const routes: Record<
string,
Core.RouterConfig | ((args: { strapi: Core.Strapi }) => Core.RouterConfig)
> = {
'content-api': ({ strapi }) => ({
type: 'content-api',
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
auth: strapi.plugin('my-plugin').config('publicRead') ? false : {},
},
},
],
}),
};
export default routes;
For details on what Strapi adds automatically at registration time, see Defaults applied by Strapi.
Defaults applied by Strapi
When Strapi registers plugin routes, it applies the following defaults automatically:
| Property | Default value | Notes |
|---|---|---|
type | 'admin' | Applied when using the array format, or when type is omitted from a router object in the named format |
prefix | '/<plugin-name>' | Applied when using the array format, or when prefix is omitted from a router object |
config.auth.scope | ['plugin::<plugin-name>.<handler>'] | Auto-generated for string handlers only, using defaultsDeep so existing values are not overwritten |
The following 2 declarations are equivalent. Strapi applies the defaults from the table above automatically:
- JavaScript
- TypeScript
module.exports = [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
},
];
module.exports = {
admin: {
type: 'admin',
prefix: '/my-plugin',
routes: [
{
method: 'GET',
path: '/articles',
handler: 'article.find',
config: {
auth: {
scope: ['plugin::my-plugin.article.find'], // auto-generated from handler string
},
},
},
],
},
};
export default [
{
method: 'GET' as const,
path: '/articles',
handler: 'article.find',
},
];
export default {
admin: {
type: 'admin' as const,
prefix: '/my-plugin',
routes: [
{
method: 'GET' as const,
path: '/articles',
handler: 'article.find',
config: {
auth: {
scope: ['plugin::my-plugin.article.find'], // auto-generated from handler string
},
},
},
],
},
};
Route configuration reference
Each route accepts an optional config object with the following properties:
policies
Type: Array<string | PolicyHandler | { name: string; options?: object }>
Policies to run before the controller action. Each item is either a policy name string, an inline function, or an object with required name and optional options.
The options object is passed as-is to the policy function's second argument (config in policy signatures). The shape of this object depends on the policy.
Plugin policies are referenced as plugin::my-plugin.policy-name.
middlewares
Type: Array<string | MiddlewareHandler | { name: string; options?: object }>
Middlewares to apply to this route. Each item is a middleware name string, an inline function, or an object with:
name: a registered middleware name,options(optional): middleware options.
At route validation time, Strapi validates middleware/policy objects against { name: string; options?: object } (see services/server/routing.ts).
The middleware resolver (services/server/middleware.ts) still contains runtime support for { resolve, config } objects, but this shape is rejected by route validation before resolution for standard plugin route declarations.
Use { name, options } in route configs for compatibility with validation.
auth
Type: false | { scope: string[]; strategies?: string[] }
Set to false to make the route public. Pass an object to define the auth scope and, optionally, custom auth strategies.
At runtime, scope must be present when auth is an object.
For string handlers (for example, handler: 'article.find'), Strapi auto-injects a default config.auth.scope value, so patterns such as auth: {} can still work.
For non-string handlers (inline functions), do not assume auto-scope injection. Define config.auth.scope explicitly when auth is an object.
Setting auth: false on an admin route is almost never intentional: it exposes the endpoint to unauthenticated requests.
For configuration examples including policies, public routes, dynamic URL parameters, and regular expressions in paths, see Routes.
Best practices
-
Use the named router format when exposing both admin and Content API endpoints. It makes the intent of each route explicit and avoids relying on the
typedefault, which can be surprising. -
Keep
handleras a string. String handlers get automatic auth scope generation, function handlers do not. Authentication still runs for both string and function handlers unless you setconfig.auth: false, but only string handlers get automaticconfig.auth.scope. If you use a function handler and need route-level permission scoping, defineconfig.auth.scopeexplicitly. -
Scope policies to their namespace. When referencing a plugin policy in a route, use the full
plugin::my-plugin.policy-nameform. This avoids ambiguity if a policy with the same short name exists elsewhere in the application. -
Do not disable auth on admin routes. Admin routes default to requiring admin authentication. Disabling auth on an admin route exposes it to unauthenticated requests, which is almost never intentional.
-
Group related routes in dedicated files. As the plugin grows, a single route index file becomes hard to navigate. Split by resource (e.g.,
routes/article.js,routes/comment.js) and re-export fromroutes/index.js.