LogoRobo.js
FrameworkPlugins

Route Definitions

Define custom source directory scanning for plugins with route files.

Route definitions are an advanced plugin API that tells Robo.js how to scan source directories and register handlers. When a plugin defines src/robo/routes/{name}.ts, Robo scans src/{name}/ in both the project and the plugin for matching files.

This is the mechanism that powers commands, events, API routes, middleware, and any other file-convention feature in Robo.js. Plugins use route definitions to extend the framework with new file-based conventions.

Most Robo.js users don't need to write route definitions. This page is for plugin authors who want to add custom file conventions to the framework.

Convention

A route definition file at src/robo/routes/{name}.ts maps to scanning src/{name}/ directories. The filename determines which directory is scanned:

commands.tsScans src/commands/
events.tsScans src/events/
api.tsScans src/api/
tasks.tsScans src/tasks/

Each route definition file can export the following:

  • A config export with RouteConfig describing how to generate keys, handle nesting, and capture exports
  • An optional default export processor function that transforms scanned entries
  • An optional controller export (factory function for runtime handler controllers)
  • Optional Handler and Controller type exports for Portal codegen

RouteConfig

The config export defines how files in the source directory are discovered and processed.

src/robo/routes/tasks.ts
import type { RouteConfig } from 'robo.js'

export const config: RouteConfig = {
	key: {
		style: 'filepath',
		separator: '/',
	},
	nesting: {
		maxDepth: 10,
		allowIndex: true,
	},
	exports: {
		default: 'required',
		config: 'optional',
		named: ['GET', 'POST'],
	},
	multiple: false,
	description: 'Scheduled task handlers',
}
src/robo/routes/tasks.js
export const config = {
	key: {
		style: 'filepath',
		separator: '/',
	},
	nesting: {
		maxDepth: 10,
		allowIndex: true,
	},
	exports: {
		default: 'required',
		config: 'optional',
		named: ['GET', 'POST'],
	},
	multiple: false,
	description: 'Scheduled task handlers',
}

Config fields

FieldTypeDefaultDescription
key.style'filename' | 'filepath' | 'parentOrFilename'-How handler keys are generated from file paths
key.separatorstring'/'Separator between key segments
key.nested'camelCase' | 'dotNotation'Uses separatorHow nested path segments are joined
key.transform(key, segments) => string-Custom transformation applied after key generation
nesting.maxDepthnumber-Maximum directory depth to scan
nesting.allowIndexbooleantrueWhether index.ts files create handlers for their parent directory
nesting.dynamicSegmentRegExp-Pattern for dynamic route segments like [id]
nesting.catchAllSegmentRegExp-Pattern for catch-all segments like [...path]
nesting.optionalCatchAllRegExp-Pattern for optional catch-all segments like [[...slug]]
exports.default'required' | 'optional' | 'forbidden''required'Whether a default export is required
exports.config'required' | 'optional' | 'forbidden''optional'Whether a config export is required
exports.namedstring[]-Named exports to capture (e.g., ['GET', 'POST'])
multiplebooleanfalseAllow multiple handlers to share the same key
singularstringRoute name without trailing 's'Singular accessor name for portal (e.g., 'command' for 'commands')
filterRegExp-Regex to filter files; non-matching files are excluded
descriptionstring-Human-readable description for documentation

Key Styles

The key.style field determines how handler keys are generated from file paths. There are three styles available:

filepath

The full relative path becomes the key. Used by API routes and sync handlers.

src/api/users/[id].ts      → "users/[id]"
src/api/health.ts           → "health"
src/api/auth/login.ts       → "auth/login"

filename

Only the filename matters, regardless of directory nesting. Used by context menu commands.

src/context/user/Profile.ts    → "Profile"
src/context/message/Report.ts  → "Report"

parentOrFilename

Uses the parent folder name for nested files and the filename for top-level files. Used by events where multiple handlers can share the same event name.

src/events/ready.ts                → "ready"
src/events/messageCreate/log.ts    → "messageCreate"
src/events/messageCreate/filter.ts → "messageCreate"

The separator field controls how path segments are joined. For example, Discord commands use a space separator:

key: { style: 'filepath', separator: ' ' }

src/commands/admin/ban.ts → "admin ban"

Processor Function

The default export transforms scanned entries into processed entries. Each file discovered in the scanned directory is passed through this function before being added to the manifest.

src/robo/routes/tasks.ts
import type { ScannedEntry, ProcessedEntry } from 'robo.js'

export default function (entry: ScannedEntry): ProcessedEntry {
	const handlerConfig = entry.exports.config as { schedule?: string } | undefined

	return {
		key: entry.key,
		path: entry.filePath.replace(/\.ts$/, '.js'),
		exports: {
			default: 'default' in entry.exports,
			config: 'config' in entry.exports,
			named: Object.keys(entry.exports).filter(
				(k) => !['default', 'config'].includes(k)
			),
		},
		metadata: {
			schedule: handlerConfig?.schedule,
		},
	}
}
src/robo/routes/tasks.js
export default function (entry) {
	const handlerConfig = entry.exports.config

	return {
		key: entry.key,
		path: entry.filePath.replace(/\.ts$/, '.js'),
		exports: {
			default: 'default' in entry.exports,
			config: 'config' in entry.exports,
			named: Object.keys(entry.exports).filter(
				(k) => !['default', 'config'].includes(k)
			),
		},
		metadata: {
			schedule: handlerConfig?.schedule,
		},
	}
}

ScannedEntry

The input to the processor. Represents a file discovered during directory scanning.

FieldTypeDescription
keystringGenerated key based on the RouteConfig key settings
typestringFull type identifier (e.g., "discord:commands")
filePathstringFile path relative to /src
relativePathstringPath relative to the route directory
exportsRecord<string, unknown>All exports from the handler file
dynamicSegments?{ params: string[], catchAll?: { param: string, optional: boolean } }Dynamic segment info (params like [id], catch-all like [...path])

ProcessedEntry

The output of the processor. Written to the manifest for runtime use.

FieldTypeDescription
keystringThe handler key
pathstringPath to the compiled handler file
exportsobjectWhich exports exist (default, config, named)
metadataRecord<string, unknown>Metadata extracted from the handler's config export
extra?Record<string, unknown>Additional data for special cases
module?stringModule name if handler is from /src/modules/
auto?booleanWhether this entry was auto-generated

Controller Factory

The controller export creates runtime handler controllers. These manage how handlers are invoked at runtime and are used by the Portal system for typed access.

src/robo/routes/tasks.ts
import type { HandlerRecord } from 'robo.js'

export interface TaskController {
	key: string
	execute: () => Promise<unknown>
}

export function controller(
	key: string,
	record: HandlerRecord,
	_pluginState: unknown
): TaskController {
	return {
		key,
		async execute() {
			const handler = record.handler as { default?: () => unknown }
			if (handler?.default) {
				return handler.default()
			}
		},
	}
}
src/robo/routes/tasks.js
export function controller(key, record, _pluginState) {
	return {
		key,
		async execute() {
			const handler = record.handler
			if (handler?.default) {
				return handler.default()
			}
		},
	}
}

Type Exports

Route files can export Handler and Controller types for Portal codegen. These types are used when generating the Portal's typed accessors, giving users type-safe access to route handlers.

src/robo/routes/tasks.ts
import type { TaskData } from '../types.js'

export type Handler = (data: TaskData) => void | Promise<void>
export type Controller = TaskController
src/robo/routes/tasks.js
// Type exports are TypeScript-only.
// JavaScript route definitions still work, but without Portal type generation.

Type exports only apply to TypeScript projects. JavaScript route definitions work the same way at runtime but do not generate typed Portal accessors.

Built-in Route Types

These route definitions are provided by official Robo.js plugins. They serve as both working examples and the foundation for Discord bots, web servers, and real-time apps.

RoutePluginScansKey StyleDescription
commands@robojs/discordjssrc/commands/filepath (space separator)Discord slash commands
events@robojs/discordjssrc/events/parentOrFilenameDiscord gateway events
middleware@robojs/discordjssrc/middleware/filepathCommand/event interceptors
context@robojs/discordjssrc/context/filenameContext menu commands
api@robojs/serversrc/api/filepathHTTP API endpoints
sync@robojs/syncsrc/sync/filepathReal-time sync handlers

Creating a Custom Route Type

This walkthrough creates a custom tasks route type that scans src/tasks/ for scheduled task handlers.

1. Create the route definition

tasks.tsDefines how src/tasks/ is scanned
cleanup.tsA task handler
report.tsAnother task handler

2. Define the route

src/robo/routes/tasks.ts
import type { RouteConfig, ScannedEntry, ProcessedEntry } from 'robo.js'

interface TaskConfig {
	schedule?: string
	description?: string
}

export const config: RouteConfig = {
	key: {
		style: 'filename',
	},
	exports: {
		default: 'required',
		config: 'optional',
	},
	description: 'Scheduled task handlers',
}

export default function (entry: ScannedEntry): ProcessedEntry {
	const handlerConfig = entry.exports.config as TaskConfig | undefined

	return {
		key: entry.key,
		path: entry.filePath.replace(/\.ts$/, '.js'),
		exports: {
			default: 'default' in entry.exports,
			config: 'config' in entry.exports,
			named: [],
		},
		metadata: {
			schedule: handlerConfig?.schedule,
			description: handlerConfig?.description,
		},
	}
}
src/robo/routes/tasks.js
export const config = {
	key: {
		style: 'filename',
	},
	exports: {
		default: 'required',
		config: 'optional',
	},
	description: 'Scheduled task handlers',
}

export default function (entry) {
	const handlerConfig = entry.exports.config

	return {
		key: entry.key,
		path: entry.filePath.replace(/\.ts$/, '.js'),
		exports: {
			default: 'default' in entry.exports,
			config: 'config' in entry.exports,
			named: [],
		},
		metadata: {
			schedule: handlerConfig?.schedule,
			description: handlerConfig?.description,
		},
	}
}

3. Create task handlers

Users (or the plugin itself) can now create files in src/tasks/ and they will be automatically discovered and processed during build.

src/tasks/cleanup.ts
export const config = {
	schedule: '0 0 * * *',
	description: 'Clean up expired sessions',
}

export default async function () {
	// Task logic here
	console.log('Running cleanup task...')
}
src/tasks/cleanup.js
export const config = {
	schedule: '0 0 * * *',
	description: 'Clean up expired sessions',
}

export default async function () {
	// Task logic here
	console.log('Running cleanup task...')
}

When robo build runs, Robo.js will scan src/tasks/, pass each file through the processor, and add the results to the manifest. The plugin can then read these entries at runtime to schedule and execute tasks.

Advanced: Namespace Controllers

Route files can also export a NamespaceController factory. This provides a higher-level API for accessing all handlers of a route type through the Portal system.

src/robo/routes/tasks.ts
import { Manifest } from 'robo.js'
import type { PortalAPI } from 'robo.js'

export interface TasksNamespaceController {
	list(): string[]
	get(name: string): Promise<unknown>
}

export const NamespaceController = (portal: PortalAPI): TasksNamespaceController => ({
	list(): string[] {
		return Manifest.routeSummariesSync('my-plugin', 'tasks').map(
			(summary) => summary.key
		)
	},

	async get(name: string): Promise<unknown> {
		const handler = await portal.getHandler('my-plugin', 'tasks', name)
		return handler?.default ?? null
	},
})
src/robo/routes/tasks.js
import { Manifest } from 'robo.js'

export const NamespaceController = (portal) => ({
	list() {
		return Manifest.routeSummariesSync('my-plugin', 'tasks').map(
			(summary) => summary.key
		)
	},

	async get(name) {
		const handler = await portal.getHandler('my-plugin', 'tasks', name)
		return handler?.default ?? null
	},
})

Namespace controllers are an advanced feature primarily used by plugins that need to expose their handlers programmatically. Most custom route types do not need one.

Next Steps

On this page