Implementing REST Service Endpoints#
Service endpoints use FastAPI via the Omniverse microservices layer. All implementations use ServiceBase from
omni.flux.service.factory.
Where Endpoints Live#
The Toolkit’s REST routes live in lightspeed.trex.service.core, one package per area under
lightspeed/trex/service/core/routes/<surface>/<area>/ — for example routes/stagecraft/assets/. Add a Toolkit route
there rather than in a new extension. Generic services that other Flux apps reuse keep their own .service extension,
such as omni.flux.validator.mass.service.
The canonical reference implementation is routes/stagecraft/assets/assets.py (AssetReplacementsService) — read it
before implementing a new service.
Service Class Rules#
The service layer is thin — all business logic goes in
.core.ServiceBase.__init__callsregister_endpoints(), so set everything the routes use before callingsuper().__init__().Mutations must go through
omni.kit.commands.execute()so they are undoable.Use Pydantic models for request/response bodies. Models the core logic also uses belong in its core extension; models only the route needs belong in the area’s
data_models/package.prefixmust be unique among the services mounted on the same router.operation_idnames the MCP tool anddescriptionis what a model reads; renaming anoperation_idis a breaking API change.
Registration and Mounting#
Registering a class makes it findable by name in the service factory; mounting makes it reachable. Toolkit routes do
both in lightspeed.trex.service.core:
Register a top-level service by adding it to
ROUTE_SERVICESinroutes/__init__.py.CoreServicemounts the top-level services named in the extension’sservicessetting.Mount an area from its top-level service.
StageCraftService(routes/stagecraft/stagecraft.py) imports each stagecraft area’s service and includes its router under/stagecraft. Areas are not registered with the factory.
StageCraftService mounts a fixed list of four areas. A new area serves nothing under /stagecraft until it joins that
list, even though its own e2e tests, which mount its router directly, still pass.
A Flux .service extension registers and unregisters its class in its own lifecycle:
from omni.flux.service.factory import get_instance as _get_service_factory_instance
from .service import MyFeatureService as _MyFeatureService
class MyServiceExtension(omni.ext.IExt):
def on_startup(self, _ext_id):
_get_service_factory_instance().register_plugins([_MyFeatureService])
def on_shutdown(self):
_get_service_factory_instance().unregister_plugins([_MyFeatureService])
Extension Dependencies#
A Toolkit route adds its backing core extension to lightspeed.trex.service.core’s [dependencies]. A Flux .service
extension declares the factory and transport:
[dependencies]
"omni.flux.service.factory" = {}
"omni.services.transport.server.base" = {}
Testing#
Service tests run against the live FastAPI router. Toolkit route tests live in lightspeed.trex.service.core under
tests/e2e/<surface>/, for example tests/e2e/stagecraft/test_assets.py. They share one Kit process with the routes
CoreService serves, so mount a test router under its own prefix and deregister it in tearDown. Test at minimum:
Happy path: correct input returns the expected response and status code.
Validation: malformed or missing input returns 422.
Side effects: mutations call the expected command and the action is undoable.