Nested Route File Structure for Strapi Plugins#
Overview#
Strapi has introduced a new nested route file structure for plugins that uses route factories and organized directory hierarchies. This structure was introduced in October 2025 and enhanced in November 2025 to improve maintainability, flexibility, and developer experience.
Route Factory Pattern#
What Changed#
Previous Approach:
- Route files exported static arrays of route objects directly
- No dynamic configuration capabilities
New Approach:
- Routes use factory functions that return route configurations
- Enables dynamic route generation and runtime configuration
- Introduced via SDK plugin PR #97
Route Factory Types#
1. Content API Routes - createContentApiRoutesFactory#
The createContentApiRoutesFactory utility creates factory functions for content API routes with backward compatibility.
Example from i18n plugin:
import { createContentApiRoutesFactory } from '@strapi/utils';
const createContentApiRoutes = createContentApiRoutesFactory((): Core.RouterInput['routes'] => {
const validator = new I18nLocaleRouteValidator(strapi);
return [
{
method: 'GET',
path: '/locales',
handler: 'locales.listLocales',
response: validator.locales,
},
];
});
export default createContentApiRoutes;
Example from users-permissions plugin:
const { createContentApiRoutesFactory } = require('@strapi/utils');
const createContentApiRoutes = createContentApiRoutesFactory(() => {
return [
...authRoutes(strapi),
...userRoutes(strapi),
...roleRoutes(strapi),
...permissionsRoutes(strapi),
];
});
module.exports = createContentApiRoutes;
Source: users-permissions plugin routes
2. Admin Routes - Direct Object Exports#
Admin routes use direct object exports with type: 'admin' property:
module.exports = {
type: 'admin',
routes: [
{
method: 'GET',
path: '/roles/:id',
handler: 'role.findOne',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
],
};
Source: users-permissions admin routes
Nested Directory Structure#
Standard Two-Tier Organization#
The recommended structure separates admin and content-api routes:
server/src/routes/
├── index.ts # Main aggregator
├── admin/
│ └── index.ts # Admin routes
└── content-api/
└── index.ts # Content API routes
Index File Aggregation#
The main index.ts file exports both route types:
TypeScript example:
import admin from './admin';
import contentApi from './content-api';
export default {
admin,
'content-api': contentApi,
};
JavaScript example:
module.exports = {
admin: require('./admin'),
'content-api': require('./content-api'),
};
Source: users-permissions plugin
Feature-Based Subdirectories#
For complex plugins, organize routes by feature within subdirectories:
server/routes/
├── index.js # Exports admin + content-api
├── admin/
│ ├── index.js # Aggregates admin routes
│ ├── role.js # Role-specific routes
│ ├── permissions.js # Permission routes
│ └── settings.js # Settings routes
└── content-api/
├── index.js # Uses createContentApiRoutesFactory
├── auth.js # Auth routes
├── user.js # User routes
└── validation.js # Validation schemas
Example: users-permissions plugin structure
Aggregating feature routes:
const permissionsRoutes = require('./permissions');
const settingsRoutes = require('./settings');
const roleRoutes = require('./role');
module.exports = {
type: 'admin',
routes: [...roleRoutes, ...settingsRoutes, ...permissionsRoutes],
};
Source: users-permissions admin aggregator
Defining Core Routes in Plugin Folders#
Plugin Server Entry Point#
Routes are exported from the main plugin server file:
import routes from './routes';
export default () => ({
routes,
// ... other exports like controllers, services, etc.
});
Route Configuration Structure#
Each route object supports:
method: HTTP method (GET, POST, PUT, DELETE, PATCH)path: Route path starting with/handler: Controller action in format'controllerName.action'config: Optional configuration:policies: Array of policies to applymiddlewares: Array of middlewaresauth: Set tofalseto bypass authentication
Example: Complete Route Definition#
export default {
type: 'admin',
routes: [
{
method: 'POST',
path: '/locales',
handler: 'locales.createLocale',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', config: { actions: ['plugin::i18n.locale.create'] } },
],
},
},
],
};
Route Registration Mechanism#
How Routes are Loaded#
The route registration process handles plugin routes:
const registerPluginRoutes = (strapi: Core.Strapi) => {
for (const pluginName of Object.keys(strapi.plugins)) {
const plugin = strapi.plugins[pluginName];
if (Array.isArray(plugin.routes)) {
// Handle direct array of routes
strapi.server.routes({
type: 'admin',
prefix: `/${pluginName}`,
routes: plugin.routes,
});
} else {
// Instantiate function-like routers
plugin.routes = instantiateRouterInputs(plugin.routes, strapi);
_.forEach(plugin.routes, (router) => {
router.type = router.type ?? 'admin';
router.prefix = router.prefix ?? `/${pluginName}`;
router.routes.forEach((route) => {
generateRouteScope(route);
route.info = { pluginName };
});
strapi.server.routes(router);
});
}
}
};
Key behaviors:
- Routes can be either an array or an object with named route groups
- Function-like routers are instantiated by calling them if they're functions
- Default
typeis 'admin' and defaultprefixis/${pluginName} - Routes are automatically scoped with
plugin::${pluginName}prefix
Benefits of the Nested Structure#
1. Dynamic Route Generation#
Route factories enable runtime configuration and conditional route setup, useful for:
- Feature toggles
- Environment-based routing
- Dynamic validation schema injection
2. Better Maintainability#
Nested structure makes navigation easier:
- Clear separation between admin and content-api routes
- Feature-based organization reduces file size
- Related routes grouped logically
3. Consistency Across Plugins#
Standardized templates and factories ensure:
- Uniform structure across all plugins
- Reduced boilerplate code
- Easier onboarding for developers
4. Backward Compatibility#
The createContentApiRoutesFactory design allows:
- Legacy extensions to mutate routes
- Gradual migration from old patterns
- No breaking changes for existing plugins
5. Type Safety#
Integration with TypeScript and Zod schemas provides:
- Better IDE support and autocomplete
- Compile-time error detection
- Validated request/response payloads
6. Enhanced Generator Support#
Enhanced plugin generators automatically:
- Create proper route file structures
- Place files in
server/src/directory - Add index files for aggregation
- Support TypeScript detection
Important Considerations#
createCoreRouter in Plugins#
When using createCoreRouter in plugins, routes must be wrapped in a content-api object:
Correct usage:
export default {
'content-api': createCoreRouter('plugin::my-plugin.model', {
config: {
findOne: { auth: false }
}
})
};
Why this matters:
- Without wrapping, routes register as admin API routes by default
- Routes won't appear in User Permissions panel
- Routes become inaccessible via standard
/api/{plugin-name}paths
File Loading Order#
- Files are loaded alphabetically
- Can cause conflicts with core routes if not carefully named
- Use subdirectories to control organization
Migration Path#
- Changes are backward-compatible
- SDK plugin version 5.4.0+ includes route folder improvements
- Existing plugins can gradually migrate to factory pattern
Complete Example: Building a Plugin with Nested Routes#
Directory Structure#
src/plugins/my-plugin/
└── server/
├── src/
│ ├── index.ts # Plugin entry point
│ ├── routes/
│ │ ├── index.ts # Route aggregator
│ │ ├── admin/
│ │ │ ├── index.ts # Admin routes
│ │ │ ├── settings.ts # Settings routes
│ │ │ └── analytics.ts # Analytics routes
│ │ └── content-api/
│ │ ├── index.ts # Content API routes factory
│ │ ├── public.ts # Public routes
│ │ └── validation.ts # Validation schemas
│ ├── controllers/
│ └── services/
└── package.json
Implementation Files#
routes/content-api/index.ts:
import { createContentApiRoutesFactory } from '@strapi/utils';
import publicRoutes from './public';
import validators from './validation';
const createContentApiRoutes = createContentApiRoutesFactory(() => {
return [
...publicRoutes(validators),
];
});
export default createContentApiRoutes;
routes/admin/index.ts:
import settingsRoutes from './settings';
import analyticsRoutes from './analytics';
export default {
type: 'admin',
routes: [
...settingsRoutes,
...analyticsRoutes,
],
};
routes/index.ts:
import admin from './admin';
import contentApi from './content-api';
export default {
admin,
'content-api': contentApi,
};
server/src/index.ts:
import routes from './routes';
import controllers from './controllers';
import services from './services';
export default () => ({
routes,
controllers,
services,
});
Summary#
The nested route file structure for Strapi plugins provides:
- Organized architecture through two-tier admin/content-api separation
- Dynamic capabilities via route factory pattern
- Better developer experience with automatic generators and type safety
- Maintainability through feature-based organization and aggregation patterns
- Backward compatibility ensuring smooth migration paths
This structure is the recommended approach for all new plugin development as of Strapi SDK plugin v5.4.0 and beyond.