Skip to main content

Project Structure

This page is the source of truth for evjs application file conventions. It also shows a practical way to organize code that the framework does not discover automatically.

my-evjs-app/
├── ev.config.ts # application-wide framework choices
├── index.html # shared HTML template
├── package.json
├── public/ # copied static files
└── src/
├── pages/
│ ├── page.tsx # /
│ ├── page.config.ts # metadata/rendering for /
│ ├── layout.tsx # root layout
│ ├── about/
│ │ └── page.tsx # /about
│ └── users/
│ ├── page.tsx # /users
│ ├── components/ # code owned by /users
│ └── $userId/
│ ├── page.tsx # /users/:userId
│ └── get-user.server.ts
├── apis/
│ ├── middleware.ts # middleware for all API routes
│ └── health/
│ └── api.ts # /health
├── middlewares/
│ ├── middleware.ts # ordered global middleware
│ └── authentication.ts
├── components/ # shared UI
├── features/ # shared business features
├── hooks/
└── lib/

The directories outside the recognized conventions are recommendations, not framework requirements. Use the organization that matches your product.

Where code belongs​

A page.* or api.* file makes a directory public:

  • page.* publishes a page and client route;
  • api.* publishes a server request route.

Everything else is ordinary source unless another documented convention names it. This means a page can safely own components, hooks, models, tests, styles, assets, and server functions in the same directory.

src/pages/orders/$orderId/
├── page.tsx # page and route
├── page.config.ts # static page choices
├── index.ts # ordinary private module
├── model.ts
├── get-order.server.ts
├── components/
│ └── Summary.tsx
└── __tests__/
└── page.test.tsx

A descendant directory with its own page.* starts another page. An _ prefix is not required. Here “private” describes route discovery and ownership, not access control.

Convention matrix​

Paths are relative to the project root unless stated otherwise.

Path or declarationMeaningImportant rules
ev.config.tsApplication configurationImport defineConfig from @evjs/ev.
conventions: falseDisables page, API route, and middleware discovery togetherOnly for applications that manage routing and runtimes themselves; cannot be combined with routing.
routing.modeEnables file-based page discovery and chooses "spa" or "mpa"The page root is always src/pages.
src/pages/**/page.{ts,tsx,js,jsx}Page and client routeExactly one variant per route directory. Default-export the React component.
<page>/page.config.{ts,js}Optional static page configurationExactly one variant beside a page.* file. Prefer definePageConfig() and TypeScript.
src/pages/**/$param/Dynamic route segmentProduces :param; SPA only.
src/pages/**/$...splat/Catch-all route segmentMust be terminal; SPA only.
src/pages/**/(group)/Pathless groupOrganizes source without changing the URL.
src/pages/**/layout.*Layout for descendant pagesComposes in SPA and MPA.
src/pages/**/error.*, not-found.*Router error and not-found boundariesSPA only.
Other files inside a page directoryPage-owned sourceDo not create routes, including index.*.
<page>/index.htmlHTML template for one MPA pageDoes not create a page or client entry.
index.html or routing.htmlShared application HTML templateindex.html is the default.
Imported module starting with "use server";Server-function moduleNamed callable exports only; no required directory.
src/apis/**/api.{ts,tsx,js,jsx}Public HTTP routeExactly one variant per directory. Export uppercase handlers; use withMiddlewares(handler, middlewares) for method-only policies.
Other files inside an API route directoryRoute-owned sourceHelpers and index.* do not create endpoints.
src/middlewares/middleware.*Global middleware compositionDefault-export one middleware or an explicitly ordered non-empty list. Computed global chains may resolve to [] when disabled.
Other files in src/middlewaresMiddleware implementation modulesImported explicitly; filenames do not define order.
public/**Static filesCopied to browser output according to output configuration.
.ev/**, dist/**, src/route-types.d.ts, src/plugin-types.d.tsGenerated outputIgnore and never edit or scaffold these files.

Page configuration​

Keep static behavior beside its page:

src/pages/orders/page.config.ts
import { definePageConfig } from "@evjs/ev";

export default definePageConfig({
title: "Orders",
meta: {
description: "Review and manage customer orders.",
},
render: "csr",
plugins: {
analytics: {
channel: "orders",
},
},
});

Core fields are title, meta, render, hydrate, prerender, rsc, and static document options. The plugins map contains page values for installed page-aware plugins. The default export must be static JSON data.

meta creates only <meta name="..." content="..."> elements. It is not a general head-element API. Rendering combinations are documented in Rendering.

A page that owns static HTML can add validated .html or .htm output aliases through document.aliases. Aliases publish the same document at another file path; they do not create another page or route.

Client path segments​

Directory nesting is route nesting:

src/pages/
├── page.tsx # /
├── teams/
│ ├── page.tsx # /teams
│ └── $teamId/
│ └── page.tsx # /teams/:teamId
├── files/
│ └── $...splat/
│ └── page.tsx # /files/*
└── (marketing)/
└── about/
└── page.tsx # /about

A directory without page.* may organize descendants. Static URL segments must start with a letter or number. evjs rejects malformed segments, duplicate paths, ambiguous dynamic shapes, and non-terminal splats.

Server route paths​

Server routes follow the same directory-owned idea under src/apis:

src/apis/
├── health/
│ └── api.ts # /health
├── users/
│ ├── api.ts # /users
│ ├── schema.ts # route-owned helper
│ └── $userId/
│ └── api.ts # /users/:userId
└── (internal)/
└── metrics/
└── api.ts # /metrics

Server route paths support static, $param, and (group) segments. Catch-all, optional, and bracket syntaxes are not supported. Page routes and API routes share the request pathname space, so conflicting patterns fail validation.

Middleware order​

Make global order explicit in src/middlewares/middleware.ts:

src/middlewares/middleware.ts
import type { MiddlewareChain } from "@evjs/ev/middleware";
import authentication from "./authentication";
import tracing from "./tracing";

export default [tracing, authentication] satisfies MiddlewareChain;

The complete order is plugin contributions, application global middleware, the method chain, and the handler. Arrays run left to right; work after await next() unwinds in reverse. Automatic OPTIONS and 405 responses run only global middleware.

Import middleware types and requestLogger from @evjs/ev/middleware. Use withMiddlewares(handler, [auth, validate]) from @evjs/ev/api for one HTTP method. Import shared chains into each method that needs them. Explicit HEAD uses its own chain; automatic HEAD uses GET's chain. Import request context helpers from @evjs/ev/server-context. See API Routes and Middleware for method behavior and the complete authoring contract.

SPA and MPA structure​

Both modes read the same page tree:

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

export default defineConfig({
routing: { mode: "spa" }, // or "mpa"
});
  • SPA supports dynamic segments, catch-alls, layouts, boundaries, and client-side navigation. An optional routing.basepath prefixes deployed browser paths without changing the Page tree or authored route paths.
  • MPA uses static page paths only and creates an independent HTML document for each page. Layouts still compose around pages.

See Pages and Routing for authoring and Rendering for delivery choices.

Shared versus colocated code​

Decide where code belongs by where it is used, not by file type:

CodeSuggested location
Used by one page or routeInside that page or API route directory
Shared by several pages in one featuresrc/features/<feature>
Shared visual primitivesrc/components
Cross-cutting utility or infrastructuresrc/lib
Static public filepublic

This convention keeps page directories understandable without turning src/pages into a collection of thin entry files.

Use an explicit route tree​

Most applications should use routing.mode and the file conventions above. Projects that need to maintain a programmatic SPA route tree can use application.routes. It cannot be combined with routing and does not support MPA.

Read Custom Routing and Runtimes before choosing that model.