Codebase Versioning
Directory-first versioning protocol for APIs, modules, routes, and reusable logic
Use directory-level versioning when the codebase must support multiple versions of the same module, route family, API, or logic set at the same time.
Directory-first rule
Do not append version numbers to filenames or component names.
Bad:
src/services/user-service-v1.ts
src/components/organisms/canvas-v2/canvas-frame.tsxGood:
src/services/v1/user-service.ts
src/components/organisms/canvas/v2/canvas-frame.tsxVersion names belong in directories (v1, v2, v3), not in leaf filenames.
This keeps imports, public names, and implementation files stable inside each
version.
Stable names across versions
This works like API versioning: the resource name stays steady while the
version lives in the path. An HTTP API exposes users at both its v2 and its
v3 path; the caller picks the contract by path. Apply the same rule to code:
the exported symbol keeps the same name in every version folder, and the
import path is what selects the version.
Bad:
// block-card/v3/block-card.tsx
export function BlockCardV3(...) {}Good:
// block-card/v2/block-card.tsx and block-card/v3/block-card.tsx
export function BlockCard(...) {}
// a file inside the owning slice selects the version via the import path
import { BlockCard } from "./v3";FSD public doors pin the live version
Inside an FSD slice, the slice's own files and stories may select a version by
its vN path. Outside the slice, consumers import through the slice's public
Interface; that door pins the one live version. An external deep import such as
#/shared/ui/molecules/stack/v3 sidesteps the public Interface even though the
version directory itself is legitimate.
During an active migration, a temporary external version path is allowed only when the migration authority names it and records its exit condition. Once the door publishes the new version, external consumers return to the door.
Do not version-suffix an export name (BlockCardV2): the version lives in
the folder, never in the symbol. If one slips in, rename it in place when it
is the only live version, or export the plain name from the new folder at the
next version bump. Storybook titles follow
the folder of the component under test (Entities/Block/BlockCard/v4), rooted
at the FSD layer — see Storybook.
When to create a version folder
Create a new version folder only when the project intentionally needs parallel support for more than one version.
Use a version folder for:
- Breaking changes — function signatures, route contracts, or data shapes change in a way that old callers cannot consume.
- API evolution — a route, endpoint, or public interface needs a new versioned surface while the old one remains available.
- Parallel support — legacy and new clients, screens, documents, agents, or integrations must run side by side.
Do not create a version folder just to try an implementation idea, rename a component, or refactor the current version in place. If there is no supported legacy surface, rename or refactor the current code instead.
Structure requirements
When a new version is required:
- Create a new folder named
vN, such asv2orv3. - Copy or refactor only the necessary logic from the previous version into the new folder.
- Add an
index.tsbarrel file inside the version folder that exports that version's public members. - Update the parent router, gateway, or public entry point to expose the new version path.
- Keep cleanup explicit: once an old version is no longer supported, remove the old folder and update docs in the same change.
Example:
src/services/user/
├── index.ts
├── v1/
│ ├── index.ts
│ ├── user-service.ts
│ └── user-service.types.ts
└── v2/
├── index.ts
├── user-service.ts
└── user-service.types.tsParent entry point:
import * as V1 from "./v1";
import * as V2 from "./v2";
export const UserService = {
v1: V1,
v2: V2,
};Type isolation
Version-specific interfaces, validators, request shapes, response shapes, and adapters should live inside the relevant version folder.
Avoid importing a mutable shared type from v1 into v2 when the type is part
of that version's public contract. Shared helpers are acceptable only when they
are genuinely version-neutral and cannot leak contract changes across versions.
Bad:
// v2/user-service.ts
import type { UserRequest } from "../v1/user-service.types";Good:
// v2/user-service.ts
import type { UserRequest } from "./user-service.types";This prevents type leakage where a v2 type change accidentally breaks v1.
Naming notes
Use product names such as Canvas v2 in user-facing documentation only when
describing a product generation or historical surface. In source paths, prefer
directory-based versioning:
src/components/organisms/canvas/v2/Do not introduce suffix-versioned folders or files. If an existing area still uses suffix naming, move it to a directory-first structure when touching that area for substantial work.