Skip to main content

File Conventions

evjs uses a small set of explicit file markers. A page.* file creates a page and client route; an api.* file creates an API route. In both trees, the containing directory determines the URL and groups the related source files.

For the complete matrix, see Project Structure.

Convention roots​

RootPurpose
src/pagesFile-based pages and client routes.
src/apisFile-based API routes.
src/middlewares/middleware.*Entry for explicitly ordered global middleware.
Imported source modulesServer functions that begin with "use server";.

Page files, API route files, and the global middleware anchor are enabled or disabled together. Top-level conventions: false turns off all of them; there are no separate switches. It cannot be combined with a routing declaration. When conventions are enabled, pages stay under src/pages and API routes stay under src/apis.

Imported "use server"; modules, SPA-only application.routes configuration, and modules generated by plugins are not controlled by this switch.

The directory of each page.* file determines its client URL. routing.mode builds the same page tree as either an SPA or an MPA.

Global styles​

Global styles are ordinary source modules, with no special filename or directory convention. Import them explicitly from the root application layout, a page, or a shared component:

import "./global.css";

Less variables and mixins follow the same rule. Import their module explicitly from each Less module that consumes them:

@import "./tokens.less";

Pages and client routes​

A page and its client route share one file marker:

src/pages/**/page.{ts,tsx,js,jsx}
export default defineConfig({
routing: {
mode: "spa",
},
});
src/pages/
├── page.tsx # /
├── page.config.ts # optional build-time config for /
├── home/
│ ├── page.tsx # /home
│ └── components/
│ ├── Hero.tsx
│ └── index.tsx # private source, not another page
└── users/
└── $userId/
├── page.tsx # /users/:userId
├── index.ts
├── model.ts
└── components/Profile.tsx

Rules:

  • exactly one supported page.* variant is allowed in a route directory;
  • directory segments relative to src/pages determine the URL;
  • the containing directory groups source owned by that page;
  • every other file, including index.*, is ordinary page source;
  • a descendant page.* intentionally creates a nested page and route;
  • the same URL pattern cannot be declared by two page files;
  • page entries default-export their component.

Page-specific code needs no _ prefix. Here, private means “not discovered as another route,” not a security boundary.

Discovery is based on explicit filenames. src/pages/home/components/index.tsx remains ordinary source because it is not named page.*; a src/pages/home/components/page.tsx would intentionally create /home/components.

An underscore does not create a private route segment. _components/Card.tsx is ordinary source because it is not named page.*, while _private/page.tsx produces an invalid-static-segment diagnostic instead of being silently ignored. Static URL segments must start with a letter or number.

Page configuration​

A page directory can include one optional page.config.ts or page.config.js. Prefer the TypeScript form:

import { definePageConfig } from "@evjs/ev";

export default definePageConfig({
title: "Home",
meta: {
description: "The application home page.",
keywords: "home,evjs",
viewport: "width=device-width, initial-scale=1",
"theme-color": "#ffffff",
},
render: "csr",
plugins: {
analytics: {
channel: "home",
},
},
});

The module is evaluated synchronously at build time. It default-exports a plain object containing static JSON data. Framework fields include title, named meta, render, hydrate, prerender, and rsc; installed plugins that support page configuration use their ids under plugins. Omitted render always means CSR, which must omit hydrate; explicit SSR/SSG pages may select "load" or "none". meta maps string keys and values only to <meta name="key" content="value">. It does not provide property, charset, links, scripts, dynamic metadata, or a general head DSL. Core title/meta are applied to the page; a plugin decides how its page values affect runtime code.

Page HTML​

The application uses top-level index.html by default; routing.html can select another shared template. In MPA mode, a page's colocated index.html overrides its HTML template. It never becomes a client entry, and SPA mode does not treat it as a route. Page title and meta add missing tags and override the matching template title and meta[name] values; omitted values keep the template defaults.

Client path segments​

Client paths come from route directories:

Directory segmentMeaning
usersStatic segment.
$userIdDynamic :userId segment.
$...splatTerminal catch-all.
(account)Pathless organization group.
src/pages/
├── page.tsx # /
├── users/
│ ├── page.tsx # /users
│ └── $userId/
│ └── page.tsx # /users/:userId
├── files/
│ └── $...splat/
│ └── page.tsx # /files/*
└── (account)/
└── settings/
└── page.tsx # /settings

SPA builds a browser route tree. MPA starts from the same pages and creates an HTML document for each static path. MPA rejects $param, terminal $...splat, and browser-router-only boundaries; layouts work in both modes.

Server functions​

Server functions have no fixed directory. The build follows modules imported by pages, layouts, wrappers, and server code.

A server-function module:

  • starts with "use server";;
  • exports named function declarations or named const function expressions;
  • does not default-export;
  • does not runtime re-export functions from another module.
"use server";

export async function getUser(userId: string) {
return { id: userId };
}

Use .server.ts or .server.tsx when colocating a server function inside a page directory so the server boundary is obvious to developers and tooling.

API routes​

API routes are discovered from api.* files under the fixed src/apis root. This convention is separate from the client page.* tree but follows the same directory-based URL model.

src/apis/
├── api.ts # /
├── api/
│ ├── health/
│ │ └── api.ts # /api/health
│ └── users/
│ ├── api.ts # /api/users
│ ├── schema.ts # private source
│ └── $userId/
│ └── api.ts # /api/users/:userId
└── (internal)/
└── metrics/
└── api.ts # /metrics

Server path segments​

Directory segmentURL meaning
$userIdDynamic parameter.
(internal)Pathless organization group.
ordinary safe nameStatic URL segment.

The api.* filename contributes no URL segment. Catch-all, optional, and bracket directory syntaxes are not supported. Static directory segments must start with a lowercase letter or number. Invalid segments are diagnosed only when their tree contains an api.* file.

Route handler exports​

Only src/apis/**/api.{ts,tsx,js,jsx} creates a route, and each route directory may contain exactly one source-extension variant. The file exports at least one uppercase HTTP method:

export function GET() {
return Response.json({ ok: true });
}

export async function POST(request: Request) {
const body = await request.json();
return Response.json(body, { status: 201 });
}

Supported methods are the framework's documented uppercase HTTP handlers. Handlers may be declared locally, imported from route-private modules, re-exported, or created by a factory. Discovery rejects values that are already statically known to be non-callable; the generated createRoute() definition validates every evaluated handler before the server starts. Generators, default exports, lowercase method names, helper exports, and route-module middleware exports are invalid in an api.* file. Every other filename is ordinary source for that route and does not publish an endpoint, regardless of its exports.

Server route conflicts​

The build rejects:

  • two files for the same normalized URL;
  • multiple api.* source-extension variants in one route directory;
  • two parameter names for the same dynamic shape, such as $id and $userId;
  • unsafe or malformed group/dynamic segments;
  • generated route-id collisions;
  • route modules that mix unsupported exports into the route contract;
  • a server request Route pattern that intersects a URL-owning Page or redirect pattern, or an active framework runtime endpoint.

Static route aliases are compared after exactly one URL decode during conflict checks. For example, /%75sers and /users claim the same request path, while double-encoded text remains distinct.

index.ts, route.ts, and foo.get.ts do not create routes.

Server middleware​

Global middleware is declared in one composition anchor:

src/middlewares/
├── middleware.ts
├── tracing.ts
└── authentication.ts
  • src/middlewares/middleware.* default-exports one global middleware or an explicitly ordered non-empty list; TypeScript lists should use satisfies MiddlewareChain, importing the type from @evjs/ev/middleware.
  • Other files in src/middlewares are ordinary modules imported by middleware.* and are not ordered by filename.

The anchor allows .ts, .tsx, .js, or .jsx, with exactly one variant in src/middlewares. Use flat arrays; reuse chains with spread. Explicit empty array exports, holes, non-functions, generators, and runtime named exports are rejected. Computed global chains may resolve to [] when disabled.

An api.* exports only uppercase HTTP methods. Use withMiddlewares(handler, middlewares) from @evjs/ev/api to compose each method's policies, importing shared chains from ordinary modules. Automatic HEAD uses GET's chain; automatic OPTIONS and 405 responses run global middleware. See API Routes and Middleware.

Generated files​

The framework may generate:

  • .ev/** framework-generated intermediate files and entries;
  • src/route-types.d.ts from SPA file routes when supported;
  • src/plugin-types.d.ts, which connects plugin types to the project's ev.config.ts;
  • dist/** build output.

Do not edit or scaffold these files. Keep them ignored.

Alternative routing inputs​

File-based routing does not require a route reader or provider. An application declares routing.mode; the presence of src/pages alone does not enable client routing.

Explicit SPA route configuration​

application cannot be combined with routing. The explicit route configuration accepts application.routes plus page or component, nested routes, layout, wrappers, and redirect. application.pageRoot controls only reference resolution for this explicit input and does not change the fixed src/pages convention root. It rejects children; nested declarations use routes. exact: true is accepted only as a terminal-match assertion; exact: false and nested routes below an exact route are rejected. Plugin configuration remains page-specific; explicit route and document objects do not expose plugin configuration. Shared template and mount values live under application.document. This configuration supports only SPA. A page reference resolves to one page.* file. An explicit component ending in index.* or page.* uses its containing directory as its source root; other component filenames are module-scoped and do not use an adjacent page.config.ts.

File-based page tree​

routing.mode discovers only page.* files. Each page entry lives in the directory for its URL; page settings live in adjacent page.config.ts files. Page-specific helpers may use any other filename, including index.*, without creating another route. Parameters, terminal catch-alls, and pathless groups use $param, $...splat, and (group) directories. Run ev inspect to review resolved pages, routes, documents, page configuration, and diagnostics.