New: Support routes.json for declarative route definitions#77
New: Support routes.json for declarative route definitions#77
Conversation
- Add schema/apirouteitem.schema.json extending routeitem with permissions - Add schema/apiroutes.schema.json extending routes with schemaName, collectionName, useDefaultRoutes - Update addRoutes() to call server's loadRouteConfig utility with handlerAliases (default/query) - Add _applyRouteConfig() to apply config from routes.json (root, schemaName, collectionName, routes) - Add _getDefaultRoutes() returning standard CRUD routes when useDefaultRoutes is true - Deprecate generateApiMetadata() for modules with routes.json - Add tests for _applyRouteConfig() and _getDefaultRoutes() Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
taylortom
left a comment
There was a problem hiding this comment.
Review
Missing schema: 'apiroutes' option
The loadRouteConfig call in addRoutes() doesn't pass schema: 'apiroutes':
const config = await serverModule.loadRouteConfig(this.rootDir, this, {
handlerAliases: { ... }
})It validates against the base routes schema only, so API-specific fields (schemaName, collectionName, useDefaultRoutes) are never validated. Should be:
const config = await loadRouteConfig(this.rootDir, this, {
schema: 'apiroutes',
handlerAliases: { ... }
})Use static import, not dynamic
The PR uses await import('adapt-authoring-server') with a ERR_MODULE_NOT_FOUND catch. Server is a peer dependency that should always be present — use a static import at the top of the file:
import { loadRouteConfig } from 'adapt-authoring-server'This fails fast at load time if the dependency is missing, rather than silently swallowing the error at runtime.
No routes.json for the module's own routes
The PR adds infrastructure to consume routes.json but doesn't include one. The default CRUD routes are still built imperatively in _getDefaultRoutes(). Adding a routes.json to the API module root is the proof that the declarative system works end-to-end.
apirouteitem schema is unnecessary
Per updated issue: permissions now lives on the base routeitem schema (with null support for unsecured routes). API routes don't need any additional per-route fields beyond what routeitem provides, so apirouteitem.schema.json can be removed. The apiroutes.schema.json should reference routeitem directly:
"routes": {
"type": "array",
"items": { "$ref": "routeitem" }
}Good
_applyRouteConfig()and_getDefaultRoutes()logic is clean- Test coverage for config application and default route generation is thorough
@deprecatedtag ongenerateApiMetadatais correct
- Add schema: 'apiroutes' to loadRouteConfig call for proper validation
- Switch from dynamic import to static namespace import of adapt-authoring-server
- Add adapt-authoring-server as devDependency
- Create routes.json with declarative default CRUD route templates (handler aliases + {scope} permission templates)
- Replace imperative _getDefaultRoutes() with template-driven implementation loading from routes.json
- Remove apirouteitem.schema.json (unnecessary); update apiroutes.schema.json to reference routeitem directly
Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
All four points addressed in 27494e0:
|
taylortom
left a comment
There was a problem hiding this comment.
Follow-up review (post-27494e0)
Good progress — schema: 'apiroutes' is now passed, routes.json is added for default CRUD templates, apirouteitem schema is removed. A few remaining issues:
1. import * as server + typeof guard is unnecessary
import * as server from 'adapt-authoring-server'
// ...
if (typeof server.loadRouteConfig === 'function') {This is a static import — if the module isn't found, the file fails to load entirely. The typeof check is dead code. Use a named import:
import { loadRouteConfig } from 'adapt-authoring-server'2. Replace createRequire with readJson from core
The PR uses createRequire(import.meta.url) to load the default routes template. Core already exports readJson which the API module already depends on:
import { AbstractModule, Hook, readJson, stringifyValues } from 'adapt-authoring-core'
// in _getDefaultRoutes() or at init time:
const template = await readJson(new URL('../routes.json', import.meta.url).pathname)This removes the createRequire import and keeps JSON loading consistent with the rest of the codebase.
3. Rename routes.json to avoid confusion
The default route template at the API module root is called routes.json but doesn't conform to the routes schema (no root field — it's a template, not a module route config). This will be confusing since routes.json in every other module root means "this module's declarative route config". Consider default-routes.json or moving it to lib/default-routes.json.
4. Variable shadowing
server is imported at module scope (import * as server) but also declared locally in addRoutes():
const server = await this.app.waitForModule('server')This shadows the import. If you switch to a named import of loadRouteConfig (point 1), this goes away.
5. adapt-authoring-server in devDependencies
It's listed under devDependencies but used in production code (AbstractApiModule.js). Should be a peerDependency to match the project's convention for inter-module dependencies.
Good
routes.jsonwith{scope}placeholder pattern is a clean approach_getDefaultRoutes()resolving handlers and permissions from the template is well-structuredapiroutes.schema.jsoncorrectly referencesrouteitemdirectly- Test coverage for
_applyRouteConfigand_getDefaultRoutesis thorough
taylortom
left a comment
There was a problem hiding this comment.
Follow-up review (post-27494e0)
Good progress — schema: 'apiroutes' is now passed, routes.json is added for default CRUD templates, apirouteitem schema is removed. A few remaining issues:
1. import * as server + typeof guard is unnecessary
import * as server from 'adapt-authoring-server'
// ...
if (typeof server.loadRouteConfig === 'function') {This is a static import — if the module isn't found, the file fails to load entirely. The typeof check is dead code. Use a named import:
import { loadRouteConfig } from 'adapt-authoring-server'2. Replace createRequire with readJson from core
The PR uses createRequire(import.meta.url) to load the default routes template. Core already exports readJson which the API module already depends on:
import { AbstractModule, Hook, readJson, stringifyValues } from 'adapt-authoring-core'
// in _getDefaultRoutes() or at init time:
const template = await readJson(new URL('../routes.json', import.meta.url).pathname)This removes the createRequire import and keeps JSON loading consistent with the rest of the codebase.
3. Rename routes.json to avoid confusion
The default route template at the API module root is called routes.json but doesn't conform to the routes schema (no root field — it's a template, not a module route config). This will be confusing since routes.json in every other module root means "this module's declarative route config". Consider default-routes.json or moving it to lib/default-routes.json.
4. Variable shadowing
server is imported at module scope (import * as server) but also declared locally in addRoutes():
const server = await this.app.waitForModule('server')This shadows the import. If you switch to a named import of loadRouteConfig (point 1), this goes away.
5. adapt-authoring-server in devDependencies
It's listed under devDependencies but used in production code (AbstractApiModule.js). Should be a peerDependency to match the project's convention for inter-module dependencies.
Good
routes.jsonwith{scope}placeholder pattern is a clean approach_getDefaultRoutes()resolving handlers and permissions from the template is well-structuredapiroutes.schema.jsoncorrectly referencesrouteitemdirectly- Test coverage for
_applyRouteConfigand_getDefaultRoutesis thorough
Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
- Replace createRequire with readJson from adapt-authoring-core - Make _getDefaultRoutes() and _applyRouteConfig() async; update tests - Rename routes.json to lib/default-routes.json to avoid confusion with consumer routes.json files - Fix variable shadowing: rename server (waitForModule) to serverMod in addRoutes() - Use optional chaining server.loadRouteConfig?.() instead of typeof guard - Restore adapt-authoring-core to dependencies (removed accidentally in previous commit) - Keep adapt-authoring-server in both peerDependencies and devDependencies (standard pattern for peer deps also needed in dev) Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com>
All five points addressed in 63e7c66:
|
Rename _applyRouteConfig to applyRouteConfig and fix _getDefaultRoutes test references to use the actual DEFAULT_ROUTES getter.
# [2.2.0](v2.1.4...v2.2.0) (2026-02-26) ### chore * planning follow-up review changes ([1791c19](1791c19)) ### Chore * Bump server peerDependency to ^2.1.0, untrack package-lock (refs #77) ([07c399d](07c399d)), closes [#77](#77) * Drop underscore prefix from internal methods (refs #77) ([7fa7665](7fa7665)), closes [#77](#77) * Fix applyRouteConfig tests for server v2.1.0 (refs #77) ([467239b](467239b)), closes [#77](#77) * Fix no-template-curly-in-string lint warnings (refs #77) ([4f12d5f](4f12d5f)), closes [#77](#77) * Use ${scope} placeholder syntax to match template literals (refs #77) ([76a789f](76a789f)), closes [#77](#77) ### feat * support routes.json for declarative route definitions ([bc1797b](bc1797b)) ### fix * address follow-up review feedback ([63e7c66](63e7c66)) * address review feedback on routes.json support ([27494e0](27494e0)) ### Fix * Add missing schema permissions, $schema declaration, deprecate validateValues ([a5aea67](a5aea67)) ### New * Support routes.json for declarative route definitions (fixes #74) (#77) ([776e621](776e621)), closes [#74](#74) [#77](#77) ### Update * Move routes.json loading into setValues for proper validation ordering ([c1ea549](c1ea549)) * Use loadRouteConfig defaults option for default route merging (refs #77) ([3ca33ae](3ca33ae)), closes [#77](#77) * Use named import for loadRouteConfig from server module ([211cdc6](211cdc6))
|
🎉 This PR is included in version 2.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Route definitions are currently spread across imperative
setValues()/addRoutes()calls and separateapidefs.jsfiles. This addsroutes.jsonsupport toAbstractApiModuleso modules can declare routes, handlers, permissions, and OpenAPI metadata in a single static file.New
schema/apiroutes.schema.json— extends server'sroutesbase schema withschemaName,collectionName,useDefaultRoutes, and typedroutesitems ($ref: routeitem)lib/default-routes.json— declarative default CRUD route template; defines the standard POST/GET/, all methods on/:_id, POST/query, and GET/schemaroutes using"default","query", and"serveSchema"handler aliases with{scope}permission placeholders expanded at runtime; lives inlib/to avoid confusion with consumer-moduleroutes.jsonfilesAbstractApiModule#_applyRouteConfig(config)— async method that applies a loaded routes.json config: setsroot/schemaName/collectionNameand builds the routes arrayAbstractApiModule#_getDefaultRoutes()— async method that loads the default CRUD route template fromlib/default-routes.jsonviareadJson, resolves handler aliases, and expands{scope}permission placeholders withthis.permissionsScope || this.rootUpdate
AbstractApiModule#addRoutes()now uses a staticimport * as server from 'adapt-authoring-server'and callsserver.loadRouteConfig?.()(optional chaining, notypeofguard) withschema: 'apiroutes'andhandlerAliasesso the string tokens"default"and"query"resolve torequestHandler()andqueryHandler()respectively; localwaitForModule('server')result renamed toserverModto avoid variable shadowing; falls back to the existing imperative path ifloadRouteConfigis not yet available on the server modulereadJsonfromadapt-authoring-corenow used for JSON loading (replacescreateRequire), keeping JSON loading consistent with the rest of the codebasegenerateApiMetadata()marked@deprecated— modules withroutes.jsonshould define metadata inline in each route'smetafield insteadadapt-authoring-serverlisted in bothpeerDependencies(production requirement) anddevDependencies(required locally for the static top-level import in tests)Testing
routes.json— verifyroot,schemaName,collectionName, androutesare populated from the file; custom routes appear after default CRUD routesuseDefaultRoutes: true(or omitted) — verify the four default route paths (/,/:_id,/query,/schema) are prepended from thelib/default-routes.jsontemplateuseDefaultRoutes: false— verify only routes defined in the module'sroutes.jsonare registered"default"/"query"handler aliases resolve to the correct bound functionsroutes.json— verify existingsetValues()+useDefaultRouteConfig()behaviour is unchangedOriginal prompt
This section details on the original issue you should resolve
<issue_title>New: Support routes.json for declarative route definitions</issue_title>
<issue_description>## Context
Route definitions are currently spread across imperative code (
setValues(),addRoutes(),useDefaultRouteConfig()) and separateapidefs.jsfiles for OpenAPI metadata. This means the API surface of each module can only be determined by running the application.This change introduces a
routes.jsonconvention — a single declarative file per module that defines routes, handlers, permissions, and OpenAPI metadata. The server module owns theroutes.jsonbase format, base schemas (routes,routeitem), and the shared loading utility (adapt-authoring-server#57). This issue covers the API module's role as a consumer — extending the base schemas, calling server's route loader, and layering on CRUD-specific semantics.Part of the broader static docs refactor: adapt-authoring-docs#47.
Related: adapt-authoring-server#57 (route loader + base schemas), adapt-authoring-auth#67 (auth consumer).
Schemas
The API module defines two schemas that extend the server base schemas via
$merge.schema/apirouteitem.schema.json— API route itemExtends the base
routeitemschema with API-specific per-route fields.{ "$anchor": "apirouteitem", "$merge": { "source": { "$ref": "routeitem" }, "with": { "properties": { "permissions": { "type": "object", "description": "Keys are HTTP methods, values are arrays of permission scope strings", "propertyNames": { "enum": ["get", "post", "put", "patch", "delete"] }, "additionalProperties": { "type": "array", "items": { "type": "string" } } } } } } }schema/apiroutes.schema.json— API route configExtends the base
routesschema with API-specific top-level fields. Overrides theroutesproperty to referenceapirouteitemfor per-route validation.{ "$anchor": "apiroutes", "$merge": { "source": { "$ref": "routes" }, "with": { "properties": { "schemaName": { "type": "string", "description": "Schema name for the module's data model" }, "collectionName": { "type": "string", "description": "MongoDB collection name" }, "useDefaultRoutes": { "type": "boolean", "description": "Whether to generate default CRUD routes", "default": true }, "routes": { "type": "array", "items": { "$ref": "apirouteitem" } } } } } }These schemas are validated by server's
loadRouteConfigutility when passedschema: 'apiroutes'.routes.jsonformat for API modules{ "root": "content", "schemaName": "content", "collectionName": "content", "useDefaultRoutes": true, "routes": [ { "route": "/insertrecursive", "handlers": { "post": "insertRecursive" }, "permissions": { "post": ["write:content"] }, "meta": { "post": { "summary": "Insert hierarchical content data", "requestBody": { "..." : "..." }, "responses": { "201": { "..." : "..." } } } } } ] }Location: Module root, alongside
adapt-authoring.json.Backward compatibility: Modules without
routes.jsonwork exactly as before —AbstractApiModulefalls back to the currentsetValues()+addRoutes()pattern. Migration is incremental.Changes to
AbstractApiModuleUse server's route loader in
addRoutes()Call server's route loading utility with the
apiroutesschema and handler aliases. Then apply API-specific concerns on top:Fall back to current behaviour if no
routes.jsonexists (backward compatibility).Resolve special handler aliases
The server utility resolves handler strings generically (
"foo"→target.foo.bind(target)). API modules have two special conventions ...💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.