Skip to main content

Custom Routing and Runtimes

Most applications should use the standard file conventions: src/pages/**/page.* creates pages, src/apis/**/api.* creates API routes, and their directories determine the URL. Global middleware comes from src/middlewares/middleware.*; HTTP method policies use withMiddlewares(handler, middlewares) from @evjs/ev/api.

Use the alternatives on this page only when an application deliberately disables file discovery, maintains a programmatic SPA route tree, or uses the client and server runtimes directly.

Disable file conventions​

File-convention discovery has one project-wide switch:

// ev.config.ts
import { defineConfig } from "@evjs/ev";

export default defineConfig({
conventions: false,
});

conventions: false disables all framework file discovery together:

  • page.* files and client routes under src/pages;
  • api.* files and API routes under src/apis;
  • global src/middlewares/middleware.*.

There are no separate switches for pages, API routes, or middleware. Do not combine conventions: false with a routing declaration. When file conventions are enabled, pages remain under src/pages and API routes remain under src/apis.

Explicit SPA application.routes configuration, imported modules marked with "use server";, and modules generated by plugins are not part of file discovery. They remain available when conventions are disabled.

The direct runtime examples below are alternatives to framework-managed file routing; they do not introduce another automatically discovered entry file.

Programmatic browser apps​

When the browser application maintains its own router and bootstrap, use the client runtime directly. The application's bundler must build this entry; evjs does not automatically discover or build src/main.tsx:

// src/main.tsx
import {
createApp,
createAppRootRoute,
createRoute,
Link,
Outlet,
} from "@evjs/client";

const rootRoute = createAppRootRoute({
component: () => (
<main>
<Link to="/">Home</Link>
<Outlet />
</main>
),
});

const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: () => <h1>Home</h1>,
});

const app = createApp({
routeTree: rootRoute.addChildren([indexRoute]),
});

declare module "@evjs/client" {
interface Register {
router: typeof app.router;
}
}

app.render("#app");

This approach is independent of the framework's file-based page model.

Embed an Application in an existing React tree​

Use app.createComponent() instead of app.render() when another React host owns the DOM root. The handle contains a stable Component, its element, and an idempotent dispose() function. The component renders the complete Application, including its Router, QueryClientProvider, and configured wrappers; creating the handle does not render or create a DOM root.

import { flushSync } from "react-dom";

const controller = new AbortController();
const application = app.createComponent({ signal: controller.signal });

// hostRoot is owned by the integrating application.
hostRoot.render(application.element);

// Remove the Application from the host tree before releasing its ownership.
flushSync(() => hostRoot.render(null));
application.dispose(); // controller.abort() also releases the handle.

Only one rendering owner is allowed. Call app.unmount() before switching from DOM rendering to component mode, and remove the component and dispose its handle before calling app.render() again. Disposing a handle or aborting its signal does not unmount the external host's React tree. An already aborted signal is rejected. The host and embedded Application must use the same React renderer.

Framework integrations using pagesApp.updateRuntime() must configure and await history changes before acquiring a component handle. Queued or pending history updates block acquisition, and an active component owner blocks history updates. Route-only updates keep the outer component stable and retire the previous Application handle after the replacement tree commits.

Programmatic server apps​

Programmatic server apps use @evjs/server directly. They are runtime primitives, not framework file-route inputs, so evjs will not scan source files for createRoute() declarations.

// src/server.ts
import { createApp, createRoute } from "@evjs/server";
import { serve } from "@evjs/server/node";

const health = createRoute("/api/health", {
GET: async () => Response.json({ ok: true }),
});

const app = createApp({
routes: [health],
});

serve(app, { port: 3001 });

Run a programmatic server runtime as a normal Node, Fetch, Bun, Deno, or platform entry outside server file-route discovery.