Контракты файлов
Контракт файла — это его роль: одна на файл, с фиксированным набором экспортов. model.server.ts — только операции с данными, types.ts — только типы, policy.ts — только правила доступа.
Контракты решают три задачи. Поиск: зная роль, вы находите код по имени файла, а не полнотекстовым поиском. Ревью: нарушение видно сразу — логика в constants.ts бросается в глаза, даже не читая дифф. Генерация: агент с скиллом FDA знает, какой файл создавать и что в него писать, — и не перемешивает слои.

🗂️ Сводная таблица
Заголовок раздела «🗂️ Сводная таблица»| Файл | Слой | Экспортирует |
|---|---|---|
schema.ts | Model | Таблицы ORM |
model.server.ts | Model | Функции операций с данными |
rpc.ts | RPC | API для других доменов |
+server.ts | RPC | HTTP-обработчики |
client.ts | RPC | Fetch-обёртки для браузера |
controller.svelte.ts / stores.ts | Controller | Реактивное состояние и обработчики |
+page.server.ts | Controller | load и actions |
+page.svelte | UI | — |
ui/ | UI | Компоненты модуля |
types.ts | — | Типы и интерфейсы |
constants.ts | — | Константы и enum |
policy.ts | — | Правила доступа |
templates.ts | — | Шаблоны форм и валидация |
utils.ts | — | Хелперы модуля |
index.ts | — | Реэкспорт публичного API |
Разберём каждый контракт. Примеры кода взяты из живого домена cart рабочего примера — имена файлов приведены для SvelteKit.
📄 Контракты по слоям
Заголовок раздела «📄 Контракты по слоям»schema.ts — описание сущностей
Заголовок раздела «schema.ts — описание сущностей»Декларативное описание таблиц ORM (Drizzle, Prisma). Никакой логики — только структура:
// lib/server/repo/db/schema.ts — пример на Drizzleimport { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const users = pgTable("users", { id: uuid("id").primaryKey().defaultRandom(), email: text("email").unique().notNull(), createdAt: timestamp("created_at").defaultNow(),});❌ Не сюда: функции работы с данными — их место в model.server.ts.
model.server.ts — операции с данными
Заголовок раздела «model.server.ts — операции с данными»Бизнес-правила домена над данными: каждый метод дёргает repo и возвращает готовые доменные объекты. Не знает ни про HTTP, ни про интерфейс:
import { db } from "$lib/server/repo/db.mock";import type { Cart, CartItem } from "./types";
export async function getCart(): Promise<Cart> { const items = await db.table<CartItem & { id: string }>("cart_items").findMany(); return calculateCart(items);}
function calculateCart(items: (CartItem & { id: string })[]): Cart { const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0); const totalCount = items.reduce((sum, i) => sum + i.quantity, 0); return { items, total, totalCount };}Модель вызывают из load-функции, из RPC или из фонового скрипта — результат одинаков. ❌ Не сюда: HTTP-обработчики и реактивное состояние.
rpc.ts — API для других доменов
Заголовок раздела «rpc.ts — API для других доменов»Публичный контракт модуля: соседние домены импортируют только отсюда. Каждый обработчик валидирует вход и вызывает одну функцию модели:
import { getCart } from "./model.server";import type { Cart } from "./types";
export async function getCartForOrder(): Promise<Cart> { return getCart();}❌ Не сюда: доступ к UI, экспорт «всего подряд» — внешний API должен быть узким и стабильным.
+server.ts — HTTP-роут
Заголовок раздела «+server.ts — HTTP-роут»Принимает запросы браузера, парсит тело, выбирает метод модели, возвращает JSON.
import { json } from "@sveltejs/kit";import * as model from "./model.server";
export const POST = async ({ request }) => { const body = await request.json(); switch (body.action) { case "add": return json(await model.addToCart(body.productId, body.quantity ?? 1)); case "remove": return json(await model.removeFromCart(body.itemId)); default: return json({ error: "Unknown action" }, { status: 400 }); }};❌ Не сюда: расчёты и правила — они в модели.
client.ts — вызовы из браузера.
Заголовок раздела «client.ts — вызовы из браузера.»Если есть rpc-клиент/генератор из swagger, то этот слой не нужен. Представляет собой тонкую обёртку над fetch: знает эндпоинты своего модуля, типов не теряет:
import type { Cart } from "./types";
export async function removeFromCart(itemId: string): Promise<Cart> { const res = await fetch("/cart", { method: "POST", body: JSON.stringify({ action: "remove", itemId }), }); return res.json();}❌ Не сюда: обработка ошибок интерфейса и состояние — это контроллер.
controller.svelte.ts, stores.ts, state.ts — реактивное состояние
Заголовок раздела «controller.svelte.ts, stores.ts, state.ts — реактивное состояние»Три имени — одна роль: хранить состояние модуля и реагировать на действия пользователя. Выбор имени зависит от фреймворка и версии: controller.svelte.ts для рун Svelte 5, stores.ts для классических сторов, composables.ts в Nuxt.
// routes/cart/stores.ts — Svelte 4 storesimport { writable, derived } from "svelte/store";import * as client from "./client";import type { Cart } from "./types";
export const cart = writable<Cart>({ items: [], total: 0, totalCount: 0 });export const isEmpty = derived(cart, ($cart) => $cart.items.length === 0);
export async function removeItem(itemId: string) { const updated = await client.removeFromCart(itemId); cart.set(updated);}Контроллер дёргает client.ts, обновляет сторы — разметка перерисовывается сама. ❌ Не сюда: импорты repo и серверной модели — иначе секреты окажутся в браузере.
+page.server.ts — серверная часть страницы
Заголовок раздела «+page.server.ts — серверная часть страницы»load отдаёт начальные данные при заходе на URL, actions принимает отправку форм. Файл создавайте, даже если он пустой, — точка входа для серверных данных пригодится:
import { getCart } from "./model.server";
export const load = async () => { const cart = await getCart(); return { cart };};❌ Не сюда: разметка и обработчики кликов.
+page.svelte — страница
Заголовок раздела «+page.svelte — страница»Только отображение. Данные — из data (их подготовил load), состояние — из контроллера, никаких fetch и расчётов:
<script lang="ts"> import { cart, removeItem, formatPrice } from "./stores";
let { data } = $props();
$effect(() => { cart.set(data.cart); });</script>
{#each $cart.items as item (item.id)} <div class="cart-item"> <div class="name">{item.name}</div> <div class="subtotal">{formatPrice(item.price * item.quantity)}</div> <button onclick={() => removeItem(item.id)}>×</button> </div>{/each}❌ Не сюда: fetch в разметке, import модели, бизнес-расчёты.
ui/ — компоненты модуля
Заголовок раздела «ui/ — компоненты модуля»Внутренние компоненты домена: CartWidget, CartItemCard, CheckoutForm. Используются только внутри своего модуля. Компонент понадобился соседям — переезжает в lib/ui/.
🧰 Вспомогательные контракты
Заголовок раздела «🧰 Вспомогательные контракты»types.ts — типы домена
Заголовок раздела «types.ts — типы домена»Только type и interface:
export interface CartItem { id: string; productId: string; name: string; price: number; image: string; quantity: number;}
export interface Cart { items: CartItem[]; total: number; totalCount: number;}❌ Не сюда: константы и функции — только типы.
constants.ts — константы и enum
Заголовок раздела «constants.ts — константы и enum»export const MAX_ITEM_QUANTITY = 99;export const MIN_ITEM_QUANTITY = 1;❌ Не сюда: функции и вычисления.
policy.ts — правила доступа
Заголовок раздела «policy.ts — правила доступа»export const canEditCart = (userId: string, cartOwnerId: string) => userId === cartOwnerId;❌ Не сюда: проверка «кто такой пользователь» — только решения «можно или нельзя».
templates.ts — шаблоны форм
Заголовок раздела «templates.ts — шаблоны форм»Схемы валидации и начальные состояния форм (zod, formik):
export const addToCartSchema = z.object({ productId: z.string(), quantity: z.number().min(1).max(99),});❌ Не сюда: обработка отправки — она в actions страницы.
utils.ts — хелперы модуля
Заголовок раздела «utils.ts — хелперы модуля»Функции, не попавшие в другие контракты: формат цены, генерация id:
export const formatPrice = (value: number) => new Intl.NumberFormat("ru-RU", { style: "currency", currency: "RUB" }).format(value);❌ Не сюда: хелперы для всего проекта — им место в lib/.
index.ts — публичный API модуля
Заголовок раздела «index.ts — публичный API модуля»Реэкспортирует внешний контракт модуля. Потребители импортируют из папки, а не из внутренних файлов:
export * from "./rpc";export * from "./controller";export * from "./types";export * from "./constants";❌ Не сюда: реэкспорт model.server.ts — серверный код не должен утекать через публичный API.
🧭 Маппинг на фреймворки
Заголовок раздела «🧭 Маппинг на фреймворки»Имена файлов роутинга зависят от мета-фреймворка, роли — нет:
| Концепция | SvelteKit | Next.js | Nuxt |
|---|---|---|---|
| Страница | +page.svelte | page.tsx | index.vue |
| Layout | +layout.svelte | layout.tsx | layout.vue |
| Server load | +page.server.ts | page.server.tsx | setup.ts |
| Server actions | +page.server.ts | actions.ts | server/ |
| API-роуты | +server.ts | route.ts | /server/api/ |
| RPC | rpc.ts | rpc.ts | rpc.ts |
| Model | model.server.ts | model.ts | model.ts |
RPC и модель называются одинаково везде — это ядро переносимости FDA.
🔗 Следующие шаги
Заголовок раздела «🔗 Следующие шаги»→ FAQ и чеклист — проверка модуля перед мержем
→ Скилл для AI-агентов — те же контракты в виде инструкций для агента