Skip to content

63946: Add "resolvable" routes for the REST API#11606

Open
rmccue wants to merge 6 commits intoWordPress:trunkfrom
rmccue:lazy-load-rest-route-options
Open

63946: Add "resolvable" routes for the REST API#11606
rmccue wants to merge 6 commits intoWordPress:trunkfrom
rmccue:lazy-load-rest-route-options

Conversation

@rmccue
Copy link
Copy Markdown
Contributor

@rmccue rmccue commented Apr 20, 2026

Adds the ability to register just-in-time resolvable routes for the REST API.

When calling register_rest_route(), you can now pass a function instead of the options for the route directly:

// Before:
register_rest_route( 'example/v1', '/ping', [
	'callback' => 'ping_callback',
	'permissions_callback' => '__return_true',
	'args' => [
		'foo' => [
			'required' => true,
			'type' => 'string',
		],
	],
	'schema' => $this->get_schema(),
] );

// After:
register_rest_route( 'example/v1', '/ping', fn () => [
	'callback' => 'ping_callback',
	'permissions_callback' => '__return_true',
	'args' => [
		'foo' => [
			'required' => true,
			'type' => 'string',
		],
	],
	'schema' => $this->get_schema(),
] );

This has the benefit that any more expensive operations (translations, args-to-schema building, etc) are only run for matched routes, rather than all of them. But, routing is still possible without this (including listing all available namespaces).

Trac ticket: https://core.trac.wordpress.org/ticket/63946

See also #10080


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props rmccue, swissspidy, kraftbj.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@rmccue
Copy link
Copy Markdown
Contributor Author

rmccue commented Apr 20, 2026

(cc @prettyboymp @kraftbj for feedback too, as an alternative to #10080)

@swissspidy
Copy link
Copy Markdown
Member

Love the ergonomics of this approach 👍

@kraftbj
Copy link
Copy Markdown

kraftbj commented Apr 20, 2026

On first glance, these two approaches seem complementary rather than competing, as they tackle 63946 at different layers of the stack.

#10080 defers at the namespace boundary: register_rest_route() never runs for a namespace that isn't matched, which also sidesteps whatever REST-only setup plugins put alongside registration — controller instantiation, register_rest_field, schema construction, boot-time translations, etc.

This one defers at the options boundary: register_rest_route() still runs for every route, but the options array (schema building, args-to-schema, translated strings inside the definition) is only materialized when that specific route is dispatched. Finer-grained & great for routes with heavy schemas, but inside the register call, not around it.

The two stack nicely. We could register a lazy namespace via #10080, and in the action that loads it, use resolvable options for any routes with expensive schemas that might still not be the one hit. Skip the namespace entirely, then if the namespace is hit, skip option construction for non-dispatched routes, and then only fully resolve the actually matched route.

tl;dr: :why-not-both.gif:

@rmccue
Copy link
Copy Markdown
Contributor Author

rmccue commented Apr 24, 2026

tl;dr: :why-not-both.gif:

The big factor here is the complexity, both conceptually and from an implementation standpoint.

The benefits from switching registration approach come from all users adopting the approach, so that we can offload as much work to just-in-time as possible. That naturally means that any new approach is going to want to be evangelised as "the" way to do it, so we should carefully consider the ergonomics.

Lazy namespaces are conceptually more complex: they require stacking multiple actions on top of each other (including a dynamic action name) which run at different times. It moves the mental model from "when the API starts, register your endpoints" to "when the API starts, register your namespace. when the namespace is being used, register your endpoints". Don't get me wrong, it's not like it's the end of the world, but if we can avoid that friction through careful design we should.

From an implementation perspective, it's also more complex - both in core, and in plugins working with it. It introduces that new "tier" above routes as an object we have to deal with, and in some ways it also moves the current $with_namespaces code from being an optimisation to being a hard part of the design. (I'd be in favour of seeing if we can actually eliminate that optimisation in favour of an approach like FastRoute.) Every new concept and system we add also has maintenance costs, so if we can avoid adding things, we should consider it.

The core thesis here really is that adding items into an array (and calling the callback that does that) isn't actually expensive, the expensive part of registering routes is building the options. If we offload that and it "solves" the performance concern, why add the complexity of lazy namespaces? That thesis is as-yet untested; to put it through its paces, I'd want to grab a selection of plugins and adapt them and do a before/after on the timing.

I take your point around controller instantiation and similar operations that happen at the registration stage, but are those actually that expensive?

(There's of course also nothing here that would block us from doing both in the future if we wanted to at that stage.)

@kraftbj
Copy link
Copy Markdown

kraftbj commented Apr 24, 2026

That's all fair. I have no objection to your approach, with the door being open to something more akin to what @prettyboymp proposed later, if we can articulate the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants