Статьи Наименования в TypeScript / Effect-ts

Наименования в TypeScript / Effect-ts

Полный свод правил именования в TypeScript / Effect-ts.

typescript effect naming

1. Философия именования

1.1 Фундаментальные принципы

Именование в функциональном программировании — это контракт между автором и читателем кода. Каждое имя должно отвечать на три вопроса:

  • Что это? (сущность, действие, свойство)
  • Какой эффект? (чистая функция, эффектная, с побочными эффектами)
  • Какова область действия? (локальная, модульная, глобальная)

1.2 Ключевые аксиомы

Аксиома 1 — Имя = Намерение. Имя должно полностью раскрывать намерение без необходимости читать реализацию.

Аксиома 2 — Глагол = Вычисление. В FP функции — это вычисления. Имя функции — это описание трансформации данных.

Аксиома 3 — Существительное = Данные. Переменные, типы, сущности — неизменяемые структуры данных. Их имена — существительные.

Аксиома 4 — Суффикс = Контекст. Суффиксы Live, Test, InMemory, Error, Schema — обязательные маркеры роли конструкции в системе.

Аксиома 5 — Префикс = Принадлежность. Префиксы модуля или домена (User, Order, Payment) группируют связанные конструкции.

1.3 Длина имени

ОбластьДлинаПример
Лямбда-параметр1–3 символаx, a, el, _
Параметр pipe/flow1–5 символовuser, req, cfg
Локальная переменная3–15 символовuserId, validatedEmail
Функция модуля5–30 символовfindUserByEmail
Тип / интерфейс5–40 символовUserRepositoryLive
Файл5–30 символовUserRepository.ts

2. Базовые правила форматирования

2.1 Таблица стилей

КонструкцияСтильПример
ПеременнаяcamelCaseuserEmail
ФункцияcamelCasecreateUser
Чистая константаcamelCasemaxRetries
Enum-like объектUPPER_SNAKEMAX_RETRY_COUNT
Тип / InterfacePascalCaseUserProfile
Class (Effect Tag/Error)PascalCaseUserNotFoundError
SchemaPascalCaseCreateUserRequest
Generic параметрPascalCase 1 буква или словоA, E, R, Self
Файл (модуль)PascalCaseUserRepository.ts
Файл (утилита/хелпер)camelCasehashPassword.ts
Директорияkebab-caseuser-management/
Env-переменнаяUPPER_SNAKEDATABASE_URL
Effect Tag identifierPascalCase строка"UserRepository"
BrandPascalCase"UserId", "Email"

2.2 Строгие запреты

// ❌ ЗАПРЕЩЕНО: венгерская нотация
const sUserName = "John"       // ❌
const iAge = 25                // ❌
const bIsActive = true         // ❌

// ✅ ПРАВИЛЬНО: семантическое именование
const userName = "John"        // ✅
const age = 25                 // ✅
const isActive = true          // ✅

// ❌ ЗАПРЕЩЕНО: аббревиатуры (кроме общепринятых)
const usrMgr = ...             // ❌
const authCtrlr = ...          // ❌
const db = ...                 // ⚠️ допустимо только в узком scope

// ✅ ПРАВИЛЬНО: полные слова
const userManager = ...        // ✅
const authController = ...     // ✅
const database = ...           // ✅

// ❌ ЗАПРЕЩЕНО: числа в именах для различения версий
const processUser2 = ...       // ❌
const handleRequestV2 = ...    // ❌

// ✅ ПРАВИЛЬНО: семантическое различие
const processUserWithRetry = ...  // ✅
const handleAuthenticatedRequest = ...  // ✅

2.3 Допустимые сокращения

СокращениеПолная формаГде допустимо
ididentifierВезде
dbdatabaseВнутри функции / лямбда
reqrequestHTTP handler, pipe
resresponseHTTP handler, pipe
cfg / configconfigurationПараметр функции
ctxcontextMiddleware, pipe
errerrorCatch-блоки, error handlers
fnfunctionHigher-order функции
accaccumulatorFold/reduce
elelementMap/filter лямбды
envenvironmentConfig, Layer
refreferenceEffect Ref
idxindexИтерация
prevpreviousReduce, state transitions
currcurrentReduce, state transitions

3. Переменные и константы

3.1 Immutable-first: всё — const

// ✅ ПРАВИЛЬНО: только const
const userName = "Vladimir" as const
const userIds = [1, 2, 3] as const
const config = { port: 3000, host: "localhost" } as const

// ❌ ЗАПРЕЩЕНО: let (кроме исключительных случаев в imperative interop)
let counter = 0  // ❌ — используй Ref в Effect

3.2 Булевы переменные

Всегда начинаются с глагольного префикса:

ПрефиксСемантикаПример
isСостояние / бытиеisActive, isValid, isEmpty
hasВладение / наличиеhasPermission, hasChildren
canВозможность / способностьcanEdit, canDelete
shouldРекомендация / ожиданиеshouldRetry, shouldNotify
wasПрошедшее состояниеwasProcessed, wasDeleted
willБудущее действиеwillExpire, willMigrate
needsНеобходимостьneedsAuth, needsValidation
didЗавершённое действиеdidComplete, didFail
allowsРазрешениеallowsGuest, allowsEmpty
// ✅ Полные примеры
const isEmailVerified = true
const hasAdminRole = pipe(user.roles, ReadonlyArray.contains("admin"))
const canAccessDashboard = isEmailVerified && hasAdminRole
const shouldSendNotification = !user.isOptedOut && hasNewMessages
const needsPasswordReset = pipe(
  lastPasswordChange,
  Duration.greaterThan(Duration.days(90))
)

3.3 Числовые переменные

// Счётчики и количества — суффикс Count
const retryCount = 3
const activeUserCount = 42
const failedAttemptCount = 0

// Максимумы/минимумы — префикс max/min
const maxRetries = 5
const minPasswordLength = 8
const maxConcurrentConnections = 100

// Размеры — суффикс Size или Length
const batchSize = 50
const bufferSize = 1024
const queueLength = 200

// Индексы — суффикс Index
const currentIndex = 0
const startIndex = 10

// Временные значения — описательные имена
const timeoutMs = 5000
const intervalSeconds = 30
const cacheTtlMinutes = 15
const sessionDurationHours = 24

3.4 Коллекции

// ✅ Множественное число для массивов и множеств
const users: ReadonlyArray<User> = []
const activeOrderIds: ReadonlySet<OrderId> = new Set()
const emailAddresses: ReadonlyArray<Email> = []

// ✅ Суффикс Map/Dict/Index для справочников
const userById: ReadonlyMap<UserId, User> = new Map()
const permissionsByRole: Record<Role, ReadonlyArray<Permission>> = {}
const priceIndex: ReadonlyMap<ProductId, Price> = new Map()

// ✅ Суффикс Queue/Stack/Buffer для структур
const taskQueue: ReadonlyArray<Task> = []
const undoStack: ReadonlyArray<Action> = []
const messageBuffer: ReadonlyArray<Message> = []

// ❌ ЗАПРЕЩЕНО
const userList = []      // ❌ — "List" избыточен
const userData = []      // ❌ — "Data" не информативен
const userArr = []       // ❌ — тип виден из типизации
const items = []         // ❌ — слишком абстрактно

3.5 Optional и Nullable

// ✅ Используем Option из Effect, имя не меняется
const maybeUser: Option.Option<User> = Option.none()
const foundUser: Option.Option<User> = Option.some(user)

// ✅ Если нужно подчеркнуть опциональность в примитивном контексте
const lastLoginAt: Option.Option<Date> = Option.none()
const parentId: Option.Option<UserId> = Option.none()

// ❌ ЗАПРЕЩЕНО: null-суффиксы, вопросительные знаки в именах
const userOrNull = null        // ❌ — используй Option
const maybeNullUser = null     // ❌

4. Функции

4.1 Чистые функции (Total Functions)

Чистые функции — глагол + существительное. Описывают трансформацию данных.

ПаттернСемантикаПример
verb + NounОсновное действиеcreateUser, validateEmail
get + NounСинхронное извлечение (чистое)getFullName, getAge
calc/compute + NounВычислениеcalcTotal, computeDiscount
parse + NounПарсинг / десериализацияparseConfig, parseDate
format + NounФорматированиеformatCurrency, formatDate
to + TargetКонвертация типаtoJSON, toString, toDTO
from + SourceСоздание из источникаfromJSON, fromDTO, fromRow
merge + NounСлияниеmergeConfigs, mergeHeaders
split + NounРазделениеsplitChunks, splitByDelimiter
filter + NounФильтрацияfilterActive, filterByRole
sort + By + CriterionСортировкаsortByCreatedAt, sortByName
group + By + KeyГруппировкаgroupByStatus, groupByDate
normalize + NounНормализацияnormalizeEmail, normalizePath
sanitize + NounОчисткаsanitizeHtml, sanitizeInput
extract + NounИзвлечение частиextractDomain, extractToken
build + NounПостроение сложного объектаbuildQuery, buildResponse
encode / decodeКодирование / декодированиеencodeBase64, decodeJwt
hash + NounХешированиеhashPassword, hashContent
// ✅ Чистые функции — существительное описывает результат
const getFullName = (user: User): string =>
  `${user.firstName} ${user.lastName}`

const calcOrderTotal = (items: ReadonlyArray<OrderItem>): Money =>
  pipe(
    items,
    ReadonlyArray.map((item) => item.price * item.quantity),
    ReadonlyArray.reduce(Money.zero, Money.add)
  )

const normalizeEmail = (raw: string): string =>
  raw.trim().toLowerCase()

const filterActiveUsers = (users: ReadonlyArray<User>): ReadonlyArray<User> =>
  pipe(users, ReadonlyArray.filter((u) => u.isActive))

const groupOrdersByStatus = (
  orders: ReadonlyArray<Order>
): ReadonlyMap<OrderStatus, ReadonlyArray<Order>> =>
  pipe(orders, ReadonlyArray.groupBy((o) => o.status))

// ✅ from/to — конвертация между типами
const fromDTO = (dto: UserDTO): User => ({
  id: UserId(dto.id),
  email: Email(dto.email),
  createdAt: new Date(dto.created_at),
})

const toResponse = (user: User): UserResponse => ({
  id: user.id,
  displayName: getFullName(user),
  memberSince: formatDate(user.createdAt),
})

4.2 Предикаты (Predicates)

Возвращают boolean. Именуются как булевы переменные.

// ✅ Предикаты — префикс is/has/can/should
const isAdult = (age: number): boolean => age >= 18

const isValidEmail = (email: string): boolean =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)

const hasPermission = (user: User, permission: Permission): boolean =>
  pipe(user.permissions, ReadonlyArray.contains(permission))

const canAccessResource = (
  user: User,
  resource: Resource
): boolean =>
  isActive(user) && hasPermission(user, resource.requiredPermission)

// ✅ Предикаты для коллекций — без скобок в pipe
const isActive = (user: User): boolean => user.isActive
const isExpired = (session: Session): boolean =>
  Date.now() > session.expiresAt

// Использование в pipe
pipe(
  users,
  ReadonlyArray.filter(isActive),
  ReadonlyArray.filter(isAdult),
)

4.3 Функции-конструкторы (Smart Constructors)

// ✅ make — основной конструктор
const makeUser = (params: {
  readonly name: string
  readonly email: string
}): User => ({
  id: UserId(generateId()),
  name: params.name,
  email: Email(params.email),
  createdAt: new Date(),
  isActive: true,
})

// ✅ makeDefault / empty — конструктор значений по умолчанию
const makeDefaultConfig = (): AppConfig => ({
  port: 3000,
  host: "localhost",
  logLevel: "info",
})

const emptyCart = (): Cart => ({
  items: [],
  total: Money.zero,
})

// ✅ of — краткий конструктор (монадический стиль)
const ofUserId = (raw: string): UserId => UserId(raw)

// ✅ unsafeMake — конструктор без валидации (для тестов/interop)
const unsafeMakeEmail = (raw: string): Email => raw as Email

4.4 Effectful функции (с побочными эффектами)

Функции, возвращающие Effect, используют те же глаголы, но Effect в возвращаемом типе явно сигнализирует о наличии эффекта. Дополнительных маркеров в имени не требуется.

// ✅ Effectful функции — имя описывает ДЕЙСТВИЕ, тип описывает ЭФФЕКТ
const findUserById = (
  id: UserId
): Effect.Effect<User, UserNotFoundError, UserRepository> =>
  pipe(
    UserRepository,
    Effect.flatMap((repo) => repo.findById(id))
  )

const sendWelcomeEmail = (
  user: User
): Effect.Effect<void, EmailDeliveryError, EmailService> =>
  pipe(
    EmailService,
    Effect.flatMap((service) => service.send({
      to: user.email,
      template: "welcome",
      data: { name: user.name },
    }))
  )

const createUserAndNotify = (
  input: CreateUserInput
): Effect.Effect<User, CreateUserError | EmailDeliveryError, UserRepository | EmailService> =>
  pipe(
    createUser(input),
    Effect.tap((user) => sendWelcomeEmail(user))
  )

Специфичные глаголы для Effect-операций:

ГлаголСемантикаПример
find + By + KeyПоиск (может не найти)findUserByEmail
fetch + NounПолучение извне (API, DB)fetchOrderDetails
save + NounСохранение / персистенцияsaveUser, saveOrder
delete + NounУдалениеdeleteSession, deleteUser
update + NounОбновлениеupdateProfile, updateStock
send + NounОтправка (email, event, msg)sendEmail, sendNotification
publish + NounПубликация событияpublishOrderCreated
subscribe + ToПодписка на событияsubscribeToUpdates
connect + ToУстановка соединенияconnectToDatabase
disconnectРазрыв соединенияdisconnectFromBroker
acquire + NounЗахват ресурса (Scope)acquireConnection
release + NounОсвобождение ресурсаreleaseConnection
retry + NounПовтор с политикойretryRequest
validate + NounВалидация (effectful)validateToken
authorize + NounАвторизацияauthorizeRequest
authenticateАутентификацияauthenticateUser
emit + NounЭмиссия событияemitMetric
schedule + NounПланированиеscheduleCleanup
spawn + NounЗапуск FiberspawnWorker
resolve + NounРазрешение / резолвresolveDependency

4.5 Higher-Order Functions

// ✅ Паттерн: withX — обёртка добавляющая поведение
const withRetry = <A, E, R>(
  policy: Schedule.Schedule<unknown, E, never>
) => (self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
  pipe(self, Effect.retry(policy))

const withTimeout = <A, E, R>(
  duration: Duration.DurationInput
) => (self: Effect.Effect<A, E, R>): Effect.Effect<A, E | TimeoutError, R> =>
  pipe(self, Effect.timeout(duration))

const withLogging = <A, E, R>(
  label: string
) => (self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
  pipe(
    Effect.log(`Starting: ${label}`),
    Effect.flatMap(() => self),
    Effect.tap((result) => Effect.log(`Completed: ${label}`))
  )

// ✅ Паттерн: makeX — фабрика функций
const makeValidator = <A>(
  schema: Schema.Schema<A>
) => (input: unknown): Effect.Effect<A, ParseError> =>
  Schema.decodeUnknown(schema)(input)

// ✅ Паттерн: onX — callback-обработчик
const onUserCreated = (handler: (user: User) => Effect.Effect<void>) =>
  pipe(
    EventBus,
    Effect.flatMap((bus) => bus.subscribe("UserCreated", handler))
  )

4.6 Функции-компараторы и эквиваленция

// ✅ Паттерн: xxxOrder / xxxEquivalence
const userByNameOrder: Order.Order<User> = pipe(
  Order.string,
  Order.mapInput((user: User) => user.name)
)

const userIdEquivalence: Equivalence.Equivalence<UserId> =
  Equivalence.string

// ✅ Паттерн: compareByXxx
const compareByCreatedAt = Order.mapInput(
  Order.Date,
  (user: User) => user.createdAt
)

// Использование
pipe(users, ReadonlyArray.sort(userByNameOrder))
pipe(users, ReadonlyArray.sort(compareByCreatedAt))

5. Effect-специфичные конструкции

5.1 Effect-программы (Programs)

// ✅ Главная программа — всегда program или main
const program: Effect.Effect<void, AppError, AppDependencies> =
  pipe(
    initializeApp,
    Effect.flatMap(() => startHttpServer),
    Effect.flatMap(() => Effect.never) // keep alive
  )

const main: Effect.Effect<void, never, never> =
  pipe(
    program,
    Effect.provide(AppLayerLive)
  )

// Запуск
BunRuntime.runMain(main)

// ✅ Под-программы — семантические имена
const startupSequence = pipe(
  runMigrations,
  Effect.flatMap(() => warmCache),
  Effect.flatMap(() => registerHealthChecks)
)

const shutdownSequence = pipe(
  drainConnections,
  Effect.flatMap(() => flushMetrics),
  Effect.flatMap(() => closeDatabase)
)

5.2 Effect.gen функции

// ✅ Внутри gen — обычные имена, yield* для получения значения
const processOrder = (orderId: OrderId) =>
  Effect.gen(function* () {
    const orderRepo = yield* OrderRepository
    const paymentService = yield* PaymentService
    const notifier = yield* NotificationService

    const order = yield* orderRepo.findById(orderId)
    const payment = yield* paymentService.charge(order.total)
    const updatedOrder = yield* orderRepo.save({
      ...order,
      status: "paid",
      paymentId: payment.id,
    })

    yield* notifier.send({
      userId: order.userId,
      type: "order_paid",
      data: { orderId: updatedOrder.id },
    })

    return updatedOrder
  })

5.3 Fiber-именование

// ✅ Fiber — суффикс Fiber
const workerFiber: Fiber.Fiber<void, WorkerError> =
  yield* Effect.fork(processQueue)

const healthCheckFiber = yield* pipe(
  checkHealth,
  Effect.repeat(Schedule.fixed("30 seconds")),
  Effect.fork
)

// ✅ FiberId annotation
const processOrder = (id: OrderId) =>
  pipe(
    doProcessOrder(id),
    Effect.withSpan(`processOrder:${id}`)
  )

6. Типы и интерфейсы

6.1 Общие правила

// ✅ PascalCase, существительное или существительное + прилагательное
type User = { ... }
type ActiveUser = { ... }
type OrderSummary = { ... }

// ✅ Суффиксы по роли
type CreateUserInput = { ... }      // входные данные
type CreateUserOutput = { ... }     // выходные данные
type UserResponse = { ... }         // HTTP response DTO
type UserRequest = { ... }          // HTTP request DTO
type UserDTO = { ... }              // Data Transfer Object
type UserRow = { ... }              // строка базы данных
type UserEvent = { ... }            // доменное событие
type UserState = { ... }            // состояние (FSM/Reducer)
type UserAction = { ... }           // действие (FSM/Reducer)
type UserCommand = { ... }          // команда (CQRS)
type UserQuery = { ... }            // запрос (CQRS)
type UserFilter = { ... }           // фильтр для запросов
type UserPatch = { ... }            // частичное обновление
type UserSnapshot = { ... }         // снимок состояния

// ❌ ЗАПРЕЩЕНО
type IUser = { ... }                // ❌ — I-prefix из C#/Java
type UserInterface = { ... }        // ❌ — "Interface" избыточно
type UserType = { ... }             // ❌ — "Type" избыточно
type TUser = { ... }                // ❌ — T-prefix

6.2 Union Types и Discriminated Unions

// ✅ Enum-like union — общее имя (существительное)
type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" | "cancelled"

type PaymentMethod = "card" | "bank_transfer" | "crypto"

// ✅ Discriminated Union — суффикс по домену
type UserEvent =
  | { readonly _tag: "UserCreated"; readonly user: User }
  | { readonly _tag: "UserUpdated"; readonly userId: UserId; readonly changes: UserPatch }
  | { readonly _tag: "UserDeleted"; readonly userId: UserId }

type PaymentResult =
  | { readonly _tag: "PaymentSuccess"; readonly transactionId: string }
  | { readonly _tag: "PaymentDeclined"; readonly reason: string }
  | { readonly _tag: "PaymentPending"; readonly checkAfter: Duration.Duration }

// ✅ Match-паттерн для DU через pipe
const handleUserEvent = (event: UserEvent) =>
  Match.value(event).pipe(
    Match.tag("UserCreated", ({ user }) => sendWelcomeEmail(user)),
    Match.tag("UserUpdated", ({ userId }) => invalidateCache(userId)),
    Match.tag("UserDeleted", ({ userId }) => cleanupUserData(userId)),
    Match.exhaustive
  )

6.3 Utility Types — именование

// ✅ Readonly-обёртки (иммутабельность)
type ReadonlyUser = Readonly<User>
type DeepReadonlyConfig = Schema.Schema.Type<typeof AppConfig>

// ✅ Pick/Omit — описательные имена
type UserCredentials = Pick<User, "email" | "passwordHash">
type PublicUserInfo = Omit<User, "passwordHash" | "internalNotes">
type UserWithoutId = Omit<User, "id">

// ✅ Partial для Patch-операций
type UserPatch = Partial<Omit<User, "id" | "createdAt">>

7. Schema (Effect Schema)

7.1 Правила именования Schema

// ✅ Schema — PascalCase, имя сущности, БЕЗ суффикса Schema
// (сама Schema — это и тип, и валидатор одновременно)
const User = Schema.Struct({
  id: Schema.String.pipe(Schema.brand("UserId")),
  email: Schema.String.pipe(Schema.nonEmptyString(), Schema.brand("Email")),
  name: Schema.String.pipe(Schema.nonEmptyString()),
  age: Schema.Number.pipe(Schema.int(), Schema.positive()),
  role: Schema.Literal("admin", "user", "moderator"),
  createdAt: Schema.Date,
  isActive: Schema.Boolean,
})

// ✅ Извлечение типа — суффикс .Type
type User = typeof User.Type

// ✅ Encoded-тип (для сериализации) — суффикс .Encoded
type UserEncoded = typeof User.Encoded

// ✅ Составные Schema — описательное имя операции
const CreateUserRequest = Schema.Struct({
  email: Schema.String.pipe(Schema.nonEmptyString()),
  name: Schema.String.pipe(Schema.nonEmptyString()),
  password: Schema.String.pipe(Schema.minLength(8)),
})

const UpdateUserRequest = Schema.Struct({
  name: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())),
  age: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive())),
})

const UserResponse = Schema.Struct({
  id: Schema.String,
  email: Schema.String,
  name: Schema.String,
  memberSince: Schema.String,
})

// ✅ Вложенные / композитные Schema
const Address = Schema.Struct({
  street: Schema.String,
  city: Schema.String,
  zipCode: Schema.String,
  country: Schema.String,
})

const UserWithAddress = Schema.extend(User, Schema.Struct({
  address: Address,
}))

// ✅ Массивы и коллекции — множественное число
const Users = Schema.Array(User)
const OrderItems = Schema.Array(OrderItem)

// ✅ Пагинация — Paginated + Entity
const PaginatedUsers = Schema.Struct({
  items: Schema.Array(User),
  total: Schema.Number.pipe(Schema.int()),
  page: Schema.Number.pipe(Schema.int(), Schema.positive()),
  pageSize: Schema.Number.pipe(Schema.int(), Schema.positive()),
})

7.2 Schema для API

// ✅ Паттерн: {Action}{Entity}{Request/Response}
const CreateUserRequest = Schema.Struct({ ... })
const CreateUserResponse = Schema.Struct({ ... })

const GetUserByIdParams = Schema.Struct({
  id: Schema.String.pipe(Schema.brand("UserId")),
})

const ListUsersQuery = Schema.Struct({
  page: Schema.optional(Schema.NumberFromString.pipe(Schema.int(), Schema.positive())),
  pageSize: Schema.optional(Schema.NumberFromString.pipe(Schema.int(), Schema.positive())),
  sortBy: Schema.optional(Schema.Literal("name", "createdAt", "email")),
  sortOrder: Schema.optional(Schema.Literal("asc", "desc")),
  search: Schema.optional(Schema.String),
})

// ✅ Паттерн: {Entity}{Action}Payload — для событий
const UserCreatedPayload = Schema.Struct({
  userId: Schema.String.pipe(Schema.brand("UserId")),
  email: Schema.String,
  timestamp: Schema.Date,
})

const OrderStatusChangedPayload = Schema.Struct({
  orderId: Schema.String.pipe(Schema.brand("OrderId")),
  fromStatus: OrderStatusSchema,
  toStatus: OrderStatusSchema,
  changedBy: Schema.String.pipe(Schema.brand("UserId")),
})

8. Ошибки (Typed Errors)

8.1 Правила именования ошибок

Строгое правило: все ошибки заканчиваются на Error.

// ✅ Доменные ошибки — {Entity}{Condition}Error
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
  "UserNotFoundError",
  {
    userId: Schema.String,
    message: Schema.optional(Schema.String),
  }
) {}

export class EmailAlreadyExistsError extends Schema.TaggedError<EmailAlreadyExistsError>()(
  "EmailAlreadyExistsError",
  {
    email: Schema.String,
  }
) {}

export class InsufficientBalanceError extends Schema.TaggedError<InsufficientBalanceError>()(
  "InsufficientBalanceError",
  {
    required: Schema.Number,
    available: Schema.Number,
  }
) {}

// ✅ Инфраструктурные ошибки — {System}{Action}Error
export class DatabaseConnectionError extends Schema.TaggedError<DatabaseConnectionError>()(
  "DatabaseConnectionError",
  {
    host: Schema.String,
    port: Schema.Number,
    cause: Schema.optional(Schema.Unknown),
  }
) {}

export class HttpRequestError extends Schema.TaggedError<HttpRequestError>()(
  "HttpRequestError",
  {
    url: Schema.String,
    method: Schema.String,
    statusCode: Schema.optional(Schema.Number),
  }
) {}

export class CacheWriteError extends Schema.TaggedError<CacheWriteError>()(
  "CacheWriteError",
  {
    key: Schema.String,
    cause: Schema.Unknown,
  }
) {}

// ✅ Бизнес-ошибки — {Process}{Reason}Error
export class PaymentDeclinedError extends Schema.TaggedError<PaymentDeclinedError>()(
  "PaymentDeclinedError",
  {
    reason: Schema.Literal("insufficient_funds", "card_expired", "fraud_detected"),
    orderId: Schema.String,
  }
) {}

export class OrderCancellationNotAllowedError extends Schema.TaggedError<OrderCancellationNotAllowedError>()(
  "OrderCancellationNotAllowedError",
  {
    orderId: Schema.String,
    currentStatus: Schema.String,
  }
) {}

8.2 Группировка ошибок

// ✅ Union-тип ошибок модуля — {Module}Error
type UserModuleError =
  | UserNotFoundError
  | EmailAlreadyExistsError
  | InvalidUserDataError

type PaymentModuleError =
  | PaymentDeclinedError
  | PaymentTimeoutError
  | PaymentProviderUnavailableError

type AppError =
  | UserModuleError
  | PaymentModuleError
  | DatabaseConnectionError

8.3 Иерархия ошибок

{Domain}Error                    — корневая ошибка домена
├── {Entity}NotFoundError        — сущность не найдена
├── {Entity}AlreadyExistsError   — дубликат
├── {Entity}ValidationError      — невалидные данные
├── {Action}NotAllowedError      — запрещённое действие
├── {Action}FailedError          — не удалось выполнить
├── {Resource}UnavailableError   — ресурс недоступен
├── {Resource}TimeoutError       — таймаут
├── {Resource}ConnectionError    — ошибка соединения
├── {Process}DeclinedError       — отказ (бизнес-правило)
├── {Auth}UnauthorizedError      — не авторизован
├── {Auth}ForbiddenError         — нет прав
├── {Data}ParseError             — ошибка парсинга
├── {Data}SerializationError     — ошибка сериализации
├── {Config}MissingError         — отсутствует конфигурация
├── {Config}InvalidError         — невалидная конфигурация
└── {Quota}ExceededError         — превышение лимита/квоты

9. Сервисы

9.1 Context.Tag — определение сервиса

// ✅ Паттерн: interface + const с одинаковым именем (companion pattern)
// Interface описывает контракт, const — это Tag для DI

// Определение интерфейса сервиса
export interface UserService {
  readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>
  readonly findByEmail: (email: Email) => Effect.Effect<User, UserNotFoundError>
  readonly create: (input: CreateUserInput) => Effect.Effect<User, EmailAlreadyExistsError>
  readonly update: (id: UserId, patch: UserPatch) => Effect.Effect<User, UserNotFoundError>
  readonly delete: (id: UserId) => Effect.Effect<void, UserNotFoundError>
  readonly list: (filter: UserFilter) => Effect.Effect<PaginatedUsers>
}

// Tag с тем же именем
export const UserService = Context.GenericTag<UserService>("UserService")

// ✅ Другие примеры сервисов
export interface EmailService {
  readonly send: (params: SendEmailParams) => Effect.Effect<void, EmailDeliveryError>
  readonly sendBatch: (params: ReadonlyArray<SendEmailParams>) => Effect.Effect<void, EmailDeliveryError>
  readonly verify: (token: string) => Effect.Effect<Email, InvalidTokenError>
}
export const EmailService = Context.GenericTag<EmailService>("EmailService")

export interface CacheService {
  readonly get: <A>(key: string) => Effect.Effect<Option.Option<A>>
  readonly set: <A>(key: string, value: A, ttl?: Duration.DurationInput) => Effect.Effect<void>
  readonly delete: (key: string) => Effect.Effect<void>
  readonly has: (key: string) => Effect.Effect<boolean>
}
export const CacheService = Context.GenericTag<CacheService>("CacheService")

9.2 Именование методов сервиса

ОперацияИмя методаВозвращает
Получить одинfindById, findByEmailEffect<A, NotFound>
Получить всеlist, findAllEffect<Array<A>>
СоздатьcreateEffect<A, AlreadyExists>
ОбновитьupdateEffect<A, NotFound>
Удалитьdelete, removeEffect<void, NotFound>
Проверитьexists, verifyEffect<boolean>
СчитатьcountEffect<number>
Поискsearch, queryEffect<Array<A>>
МассовыеcreateBatch, deleteBatchEffect<Array<A>>

10. Репозитории

10.1 Repository Pattern в Effect

// ✅ Интерфейс — {Entity}Repository
export interface UserRepository {
  // Queries
  readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>
  readonly findByEmail: (email: Email) => Effect.Effect<Option.Option<User>>
  readonly findAll: (filter: UserFilter) => Effect.Effect<ReadonlyArray<User>>
  readonly count: (filter: UserFilter) => Effect.Effect<number>
  readonly exists: (id: UserId) => Effect.Effect<boolean>

  // Commands
  readonly insert: (user: User) => Effect.Effect<User, EmailAlreadyExistsError>
  readonly update: (id: UserId, patch: UserPatch) => Effect.Effect<User, UserNotFoundError>
  readonly delete: (id: UserId) => Effect.Effect<void, UserNotFoundError>
  readonly insertBatch: (users: ReadonlyArray<User>) => Effect.Effect<ReadonlyArray<User>>
}

export const UserRepository = Context.GenericTag<UserRepository>("UserRepository")

10.2 Реализации репозитория

Строгое правило: суффикс реализации = технология / стратегия.

// ✅ Production — суффикс технологии или Live
export const UserRepositoryPostgres: Layer.Layer<UserRepository, never, DatabaseClient> = ...
export const UserRepositorySurrealDb: Layer.Layer<UserRepository, never, SurrealClient> = ...
export const UserRepositoryLive: Layer.Layer<UserRepository, never, DatabaseClient> = ... // когда технология одна

// ✅ Тестовые — суффикс стратегии
export const UserRepositoryInMemory: Layer.Layer<UserRepository> = ...
export const UserRepositoryStub: Layer.Layer<UserRepository> = ...        // фиксированные данные
export const UserRepositoryFailing: Layer.Layer<UserRepository> = ...     // всегда ошибка (для тестов)

// ✅ Файлы реализаций
// UserRepository.ts          — интерфейс + Tag
// UserRepositoryLive.ts      — production-реализация
// UserRepositoryInMemory.ts  — тестовая реализация

11. Слои (Layer)

11.1 Правила именования Layer

// ✅ Паттерн: {Service}{Implementation}
// Layer содержит реализацию сервиса, его имя совпадает с реализацией

// Простые Layer
const UserRepositoryLive: Layer.Layer<UserRepository, never, DatabaseClient> =
  Layer.effect(
    UserRepository,
    Effect.gen(function* () {
      const db = yield* DatabaseClient
      return { /* implementation */ }
    })
  )

// ✅ Составные Layer — {Module}Live / {Feature}Live
const UserModuleLive: Layer.Layer<
  UserRepository | UserService | UserCache,
  never,
  DatabaseClient | CacheService
> = Layer.mergeAll(
  UserRepositoryLive,
  UserServiceLive,
  UserCacheLive,
)

// ✅ Полный граф приложения — AppLive / AppLayer
const AppLive: Layer.Layer<AppDependencies> = pipe(
  Layer.mergeAll(
    UserModuleLive,
    OrderModuleLive,
    PaymentModuleLive,
  ),
  Layer.provideMerge(InfrastructureLive),
  Layer.provideMerge(ConfigLive),
)

// ✅ Тестовые Layer — {Module}Test
const UserModuleTest: Layer.Layer<UserRepository | UserService> =
  Layer.mergeAll(
    UserRepositoryInMemory,
    UserServiceLive,
  )

11.2 Layer-файлы

src/
├── layers/
│   ├── AppLive.ts              # Главный Layer приложения
│   ├── InfrastructureLive.ts   # БД, кеш, очереди
│   ├── ConfigLive.ts           # Конфигурация
│   └── ObservabilityLive.ts    # Логирование, трейсинг, метрики

12. Доменные сущности и Value Objects

12.1 Entity

// ✅ Entity — существительное, PascalCase
// Определяется через Schema для runtime-валидации
const User = Schema.Struct({
  id: UserId,
  email: Email,
  name: UserName,
  role: UserRole,
  createdAt: Schema.Date,
  updatedAt: Schema.Date,
})
type User = typeof User.Type

const Order = Schema.Struct({
  id: OrderId,
  userId: UserId,
  items: Schema.Array(OrderItem),
  status: OrderStatus,
  total: Money,
  createdAt: Schema.Date,
})
type Order = typeof Order.Type

12.2 Value Object

// ✅ Value Object — описательное имя, всегда через Schema + Brand
const Email = Schema.String.pipe(
  Schema.nonEmptyString(),
  Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
  Schema.brand("Email"),
)
type Email = typeof Email.Type

const Money = Schema.Struct({
  amount: Schema.Number.pipe(Schema.finite()),
  currency: Schema.Literal("USD", "EUR", "RUB"),
})
type Money = typeof Money.Type

const DateRange = Schema.Struct({
  from: Schema.Date,
  to: Schema.Date,
}).pipe(
  Schema.filter(({ from, to }) => from <= to, {
    message: () => "from must be before to"
  })
)
type DateRange = typeof DateRange.Type

const Percentage = Schema.Number.pipe(
  Schema.greaterThanOrEqualTo(0),
  Schema.lessThanOrEqualTo(100),
  Schema.brand("Percentage"),
)
type Percentage = typeof Percentage.Type

const NonNegativeInt = Schema.Number.pipe(
  Schema.int(),
  Schema.nonNegative(),
  Schema.brand("NonNegativeInt"),
)
type NonNegativeInt = typeof NonNegativeInt.Type

12.3 Domain Events

// ✅ Паттерн: {Entity}{PastTenseVerb}
const UserCreated = Schema.TaggedStruct("UserCreated", {
  userId: UserId,
  email: Email,
  occurredAt: Schema.Date,
})
type UserCreated = typeof UserCreated.Type

const OrderShipped = Schema.TaggedStruct("OrderShipped", {
  orderId: OrderId,
  trackingNumber: Schema.String,
  carrier: Schema.String,
  shippedAt: Schema.Date,
})
type OrderShipped = typeof OrderShipped.Type

const PaymentReceived = Schema.TaggedStruct("PaymentReceived", {
  orderId: OrderId,
  amount: Money,
  method: PaymentMethod,
  receivedAt: Schema.Date,
})
type PaymentReceived = typeof PaymentReceived.Type

// ✅ Union всех событий домена
type DomainEvent =
  | UserCreated
  | UserUpdated
  | UserDeleted
  | OrderCreated
  | OrderShipped
  | PaymentReceived

13. Branded Types

13.1 Правила

// ✅ Brand-строка = PascalCase, совпадает с именем типа
// Branded type — это тег компилятора для type safety

const UserId = Schema.String.pipe(
  Schema.nonEmptyString(),
  Schema.brand("UserId")
)
type UserId = typeof UserId.Type

const OrderId = Schema.String.pipe(
  Schema.nonEmptyString(),
  Schema.brand("OrderId")
)
type OrderId = typeof OrderId.Type

const ProductId = Schema.String.pipe(
  Schema.nonEmptyString(),
  Schema.brand("ProductId")
)
type ProductId = typeof ProductId.Type

// ✅ Branded числа
const PositiveInt = Schema.Number.pipe(
  Schema.int(),
  Schema.positive(),
  Schema.brand("PositiveInt")
)
type PositiveInt = typeof PositiveInt.Type

const Port = Schema.Number.pipe(
  Schema.int(),
  Schema.greaterThanOrEqualTo(1),
  Schema.lessThanOrEqualTo(65535),
  Schema.brand("Port")
)
type Port = typeof Port.Type

// ✅ Branded-строки с валидацией
const Url = Schema.String.pipe(
  Schema.pattern(/^https?:\/\/.+/),
  Schema.brand("Url")
)
type Url = typeof Url.Type

const JwtToken = Schema.String.pipe(
  Schema.nonEmptyString(),
  Schema.brand("JwtToken")
)
type JwtToken = typeof JwtToken.Type

// ❌ ЗАПРЕЩЕНО
type UserId = string & Brand.Brand<"userId">   // ❌ — бренд в camelCase
type ID = string & Brand.Brand<"ID">           // ❌ — абстрактное имя

14. Pipe, Flow и композиция

14.1 Pipeline-именование

// ✅ Промежуточные результаты в pipe — описательные имена
const processRegistration = (input: RegistrationInput) =>
  pipe(
    validateRegistration(input),                    // Effect<ValidatedInput, ValidationError>
    Effect.flatMap(normalizeRegistrationData),       // Effect<NormalizedInput, ValidationError>
    Effect.flatMap(checkEmailUniqueness),            // Effect<NormalizedInput, EmailAlreadyExistsError>
    Effect.flatMap(createUserFromRegistration),      // Effect<User, CreateUserError>
    Effect.tap(sendVerificationEmail),               // Effect<User, EmailDeliveryError>
    Effect.tap(publishUserCreatedEvent),             // Effect<User, EventPublishError>
    Effect.map(toRegistrationResponse),              // Effect<RegistrationResponse, ...>
  )

// ✅ Flow — именование как функция, описывает полный pipeline
const processPayment: (input: PaymentInput) => Effect.Effect<PaymentResult, PaymentError, PaymentDependencies> =
  flow(
    validatePaymentInput,
    Effect.flatMap(resolvePaymentMethod),
    Effect.flatMap(executePayment),
    Effect.flatMap(recordTransaction),
  )

14.2 Лямбды в pipe

// ✅ Короткие лямбды — 1 параметр, без скобок
pipe(users, ReadonlyArray.map((u) => u.name))
pipe(users, ReadonlyArray.filter((u) => u.isActive))
pipe(numbers, ReadonlyArray.reduce(0, (acc, n) => acc + n))

// ✅ Ссылки на функции вместо лямбд (point-free стиль)
pipe(users, ReadonlyArray.map(getFullName))
pipe(users, ReadonlyArray.filter(isActive))
pipe(userIds, ReadonlyArray.map(findUserById))

// ❌ ЗАПРЕЩЕНО: бессмысленные имена в лямбдах
pipe(users, ReadonlyArray.map((x) => x.name))        // ❌ — "x" не информативен
pipe(users, ReadonlyArray.map((item) => item.name))   // ⚠️ — "item" слишком обобщённо
pipe(users, ReadonlyArray.map((data) => data.name))   // ❌ — "data" бессмысленно

15. Stream, Queue, Pub/Sub

15.1 Stream

// ✅ Stream-переменные — описание потока данных
const userActivityStream: Stream.Stream<UserActivity, DatabaseError, DatabaseClient> =
  pipe(
    Stream.fromEffect(fetchRecentActivity),
    Stream.flatMap((activities) => Stream.fromIterable(activities)),
  )

const logStream: Stream.Stream<LogEntry> = ...
const priceUpdateStream: Stream.Stream<PriceUpdate, WebSocketError> = ...
const orderEventStream: Stream.Stream<OrderEvent, never, EventBus> = ...

// ✅ Stream-функции — глагол + "Stream" или описательное имя
const streamUserEvents = (userId: UserId): Stream.Stream<UserEvent> => ...
const watchPriceChanges = (productId: ProductId): Stream.Stream<PriceUpdate> => ...
const tailLogFile = (path: string): Stream.Stream<string, FileError> => ...

15.2 Queue

// ✅ Queue-переменные — суффикс Queue
const taskQueue: Queue.Queue<Task> = ...
const notificationQueue: Queue.Queue<Notification> = ...
const deadLetterQueue: Queue.Queue<FailedMessage> = ...

// ✅ Dequeue/Enqueue — описание направления
const incomingMessages: Queue.Enqueue<Message> = ...
const processedResults: Queue.Dequeue<Result> = ...

15.3 PubSub / EventBus

// ✅ Topic/Channel — описательное имя
const userEventsTopic: PubSub.PubSub<UserEvent> = ...
const orderUpdatesChannel: PubSub.PubSub<OrderUpdate> = ...

// ✅ Паттерн: {domain}Events{Topic/Channel}
const paymentEventsChannel = yield* PubSub.unbounded<PaymentEvent>()

16. Config и Environment

16.1 Config-схемы

// ✅ Паттерн: {Module}Config
const DatabaseConfig = Schema.Struct({
  host: Schema.String,
  port: Port,
  database: Schema.String,
  username: Schema.String,
  password: Schema.Redacted(Schema.String),
  maxConnections: Schema.optional(PositiveInt).pipe(
    Schema.withDefault(() => PositiveInt.make(10))
  ),
  connectionTimeoutMs: Schema.optional(Schema.Number).pipe(
    Schema.withDefault(() => 5000)
  ),
})
type DatabaseConfig = typeof DatabaseConfig.Type

const HttpServerConfig = Schema.Struct({
  port: Port,
  host: Schema.String,
  corsOrigins: Schema.Array(Schema.String),
  requestTimeoutMs: Schema.Number,
})
type HttpServerConfig = typeof HttpServerConfig.Type

const AppConfig = Schema.Struct({
  database: DatabaseConfig,
  http: HttpServerConfig,
  logLevel: Schema.Literal("debug", "info", "warn", "error"),
  environment: Schema.Literal("development", "staging", "production"),
})
type AppConfig = typeof AppConfig.Type

16.2 Config Tag

// ✅ Config как сервис
const AppConfig = Context.GenericTag<AppConfig>("AppConfig")
const DatabaseConfig = Context.GenericTag<DatabaseConfig>("DatabaseConfig")

// ✅ Config Layer
const AppConfigLive: Layer.Layer<AppConfig> = Layer.effect(
  AppConfig,
  Effect.gen(function* () {
    const raw = yield* Effect.config(Config.string("APP_CONFIG_PATH"))
    // ... load and parse
  })
)

16.3 Environment Variables

// ✅ UPPER_SNAKE_CASE, префикс по модулю/приложению
// APP_ — общие приложения
// DB_  — база данных
// HTTP_ — HTTP сервер
// AUTH_ — аутентификация
// CACHE_ — кеш

const envConfig = {
  APP_ENV: Config.string("APP_ENV"),
  APP_LOG_LEVEL: Config.string("APP_LOG_LEVEL"),
  DB_HOST: Config.string("DB_HOST"),
  DB_PORT: Config.integer("DB_PORT"),
  DB_PASSWORD: Config.redacted("DB_PASSWORD"),
  HTTP_PORT: Config.integer("HTTP_PORT"),
  AUTH_JWT_SECRET: Config.redacted("AUTH_JWT_SECRET"),
  AUTH_TOKEN_TTL_SECONDS: Config.integer("AUTH_TOKEN_TTL_SECONDS"),
  CACHE_REDIS_URL: Config.string("CACHE_REDIS_URL"),
} as const

17. HTTP

17.1 HttpRouter и Routes

// ✅ Паттерн: {Entity}Routes / {Entity}Router
const UserRoutes = HttpRouter.empty.pipe(
  HttpRouter.get("/users", listUsersHandler),
  HttpRouter.get("/users/:id", getUserByIdHandler),
  HttpRouter.post("/users", createUserHandler),
  HttpRouter.patch("/users/:id", updateUserHandler),
  HttpRouter.del("/users/:id", deleteUserHandler),
)

const OrderRoutes = HttpRouter.empty.pipe(
  HttpRouter.get("/orders", listOrdersHandler),
  HttpRouter.get("/orders/:id", getOrderByIdHandler),
  HttpRouter.post("/orders", createOrderHandler),
)

// ✅ Главный роутер — AppRouter / ApiRouter
const ApiRouter = HttpRouter.empty.pipe(
  HttpRouter.mount("/api/v1", pipe(
    HttpRouter.empty,
    HttpRouter.mount("/users", UserRoutes),
    HttpRouter.mount("/orders", OrderRoutes),
    HttpRouter.mount("/payments", PaymentRoutes),
  )),
)

// ✅ Health/System роутер
const SystemRouter = HttpRouter.empty.pipe(
  HttpRouter.get("/health", healthHandler),
  HttpRouter.get("/ready", readinessHandler),
  HttpRouter.get("/metrics", metricsHandler),
)

17.2 Handlers

// ✅ Паттерн: {action}{Entity}Handler
const listUsersHandler = HttpRouter.makeHandler(
  Effect.gen(function* () {
    const userService = yield* UserService
    const users = yield* userService.list({})
    return HttpServerResponse.json(users)
  })
)

const getUserByIdHandler = HttpRouter.makeHandler(
  Effect.gen(function* () {
    const params = yield* HttpRouter.params
    const userService = yield* UserService
    const user = yield* userService.findById(UserId(params.id))
    return HttpServerResponse.json(toUserResponse(user))
  })
)

const createUserHandler = HttpRouter.makeHandler(
  Effect.gen(function* () {
    const body = yield* HttpServerRequest.schemaBodyJson(CreateUserRequest)
    const userService = yield* UserService
    const user = yield* userService.create(body)
    return HttpServerResponse.json(toUserResponse(user), { status: 201 })
  })
)

18. RPC

18.1 RPC-определения

// ✅ Паттерн: {Action}{Entity} — имя запроса как действие
const GetUser = Rpc.make("GetUser", {
  payload: Schema.Struct({ id: UserId }),
  success: User,
  error: UserNotFoundError,
})

const CreateUser = Rpc.make("CreateUser", {
  payload: CreateUserRequest,
  success: User,
  error: Schema.Union(EmailAlreadyExistsError, InvalidUserDataError),
})

const ListUsers = Rpc.make("ListUsers", {
  payload: ListUsersQuery,
  success: PaginatedUsers,
  error: Schema.Never,
})

// ✅ Группа RPC — {Module}Rpcs
const UserRpcs = RpcGroup.make("UserRpcs", {
  GetUser,
  CreateUser,
  ListUsers,
  UpdateUser,
  DeleteUser,
})

// ✅ Реализация — {Module}RpcsLive
const UserRpcsLive = UserRpcs.toLayer(
  Effect.gen(function* () {
    const userService = yield* UserService
    return {
      GetUser: ({ id }) => userService.findById(id),
      CreateUser: (input) => userService.create(input),
      ListUsers: (query) => userService.list(query),
      UpdateUser: ({ id, ...patch }) => userService.update(id, patch),
      DeleteUser: ({ id }) => userService.delete(id),
    }
  })
)

19. Middleware

19.1 Именование Middleware

// ✅ Паттерн: {Concern}Middleware
const AuthMiddleware = HttpMiddleware.make((app) =>
  Effect.gen(function* () {
    const request = yield* HttpServerRequest.HttpServerRequest
    const token = extractBearerToken(request)
    const user = yield* verifyToken(token)
    return yield* app.pipe(
      Effect.provideService(CurrentUser, user)
    )
  })
)

const LoggingMiddleware = HttpMiddleware.make((app) =>
  Effect.gen(function* () {
    const request = yield* HttpServerRequest.HttpServerRequest
    const start = yield* Clock.currentTimeMillis
    const response = yield* app
    const duration = (yield* Clock.currentTimeMillis) - start
    yield* Effect.log(`${request.method} ${request.url} ${response.status} ${duration}ms`)
    return response
  })
)

const CorsMiddleware = HttpMiddleware.make((app) => ...)
const RateLimitMiddleware = HttpMiddleware.make((app) => ...)
const CompressionMiddleware = HttpMiddleware.make((app) => ...)
const TimeoutMiddleware = HttpMiddleware.make((app) => ...)
const ErrorBoundaryMiddleware = HttpMiddleware.make((app) => ...)
const TracingMiddleware = HttpMiddleware.make((app) => ...)
const CacheControlMiddleware = HttpMiddleware.make((app) => ...)

// ✅ Composition — pipe
const MiddlewareStack = flow(
  ErrorBoundaryMiddleware,
  LoggingMiddleware,
  TracingMiddleware,
  CorsMiddleware,
  AuthMiddleware,
  RateLimitMiddleware,
)

20. Тесты

20.1 Именование тестов

// ✅ describe — модуль/функция
// ✅ it — поведение на естественном языке

describe("UserService", () => {
  describe("create", () => {
    it("should create a user with valid input", () =>
      Effect.gen(function* () {
        const service = yield* UserService
        const user = yield* service.create(validInput)
        expect(user.email).toBe(validInput.email)
      }).pipe(
        Effect.provide(UserModuleTest),
        Effect.runPromise
      )
    )

    it("should fail with EmailAlreadyExistsError for duplicate email", () =>
      Effect.gen(function* () {
        const service = yield* UserService
        yield* service.create(validInput)
        const result = yield* service.create(validInput).pipe(Effect.either)
        expect(Either.isLeft(result)).toBe(true)
      }).pipe(
        Effect.provide(UserModuleTest),
        Effect.runPromise
      )
    )

    it("should hash password before storing", () => ...)
    it("should publish UserCreated event", () => ...)
    it("should set default role to 'user'", () => ...)
  })
})

20.2 Тестовые утилиты

// ✅ Паттерн: make{Entity}Fixture / fake{Entity} / stub{Entity}
const makeUserFixture = (overrides?: Partial<User>): User => ({
  id: UserId("test-user-id"),
  email: Email("test@example.com"),
  name: "Test User",
  role: "user",
  isActive: true,
  createdAt: new Date("2024-01-01"),
  updatedAt: new Date("2024-01-01"),
  ...overrides,
})

const makeOrderFixture = (overrides?: Partial<Order>): Order => ({
  id: OrderId("test-order-id"),
  userId: UserId("test-user-id"),
  items: [],
  status: "pending",
  total: { amount: 0, currency: "USD" },
  createdAt: new Date("2024-01-01"),
  ...overrides,
})

// ✅ Генераторы — generate{Entity}
const generateUserId = (): UserId => UserId(crypto.randomUUID())
const generateEmail = (prefix = "test"): Email =>
  Email(`${prefix}-${crypto.randomUUID().slice(0, 8)}@example.com`)

21. Файлы и модули

21.1 Правила именования файлов

СодержимоеСтиль файлаПример
Entity / SchemaPascalCaseUser.ts, Order.ts
Service interface + TagPascalCaseUserService.ts
Service implementationPascalCaseUserServiceLive.ts
Repository interface + TagPascalCaseUserRepository.ts
Repository implementationPascalCaseUserRepositoryLive.ts
Error definitionsPascalCaseUserErrors.ts
Layer compositionPascalCaseAppLive.ts
Pure utility functioncamelCasehashPassword.ts
Helper / shared utilitycamelCaseformatDate.ts
ConstantscamelCasehttpStatus.ts
Config schemaPascalCaseAppConfig.ts
HTTP RoutesPascalCaseUserRoutes.ts
HTTP HandlersPascalCaseUserHandlers.ts
MiddlewarePascalCaseAuthMiddleware.ts
Test filePascalCaseUserService.test.ts
Index / barrel exportcamelCaseindex.ts

21.2 Один файл — одна ответственность

// ✅ Файл UserRepository.ts — ТОЛЬКО интерфейс и Tag
export interface UserRepository { ... }
export const UserRepository = Context.GenericTag<UserRepository>("UserRepository")

// ✅ Файл UserRepositoryLive.ts — ТОЛЬКО production-реализация
export const UserRepositoryLive: Layer.Layer<UserRepository, never, DatabaseClient> = ...

// ✅ Файл UserErrors.ts — ВСЕ ошибки модуля
export class UserNotFoundError extends ...
export class EmailAlreadyExistsError extends ...
export class InvalidUserDataError extends ...
export type UserModuleError = UserNotFoundError | EmailAlreadyExistsError | InvalidUserDataError

// ✅ Файл index.ts — barrel export
export { UserService } from "./UserService.js"
export type { UserService as UserServiceType } from "./UserService.js"
export { UserServiceLive } from "./UserServiceLive.js"
export { UserRepository } from "./UserRepository.js"
export { UserRepositoryLive } from "./UserRepositoryLive.js"
export * from "./UserErrors.js"
export { User, CreateUserRequest, UserResponse } from "./schemas/index.js"

22. Директории и архитектура проекта

22.1 Feature-Sliced / Hexagonal

src/
├── modules/                        # Бизнес-модули (kebab-case)
│   ├── user-management/            # Домен: управление пользователями
│   │   ├── domain/                 # Чистый домен (нет зависимостей)
│   │   │   ├── entities/           # Сущности
│   │   │   │   └── User.ts
│   │   │   ├── value-objects/      # Value Objects
│   │   │   │   ├── Email.ts
│   │   │   │   ├── UserId.ts
│   │   │   │   └── UserName.ts
│   │   │   ├── events/             # Доменные события
│   │   │   │   ├── UserCreated.ts
│   │   │   │   └── UserUpdated.ts
│   │   │   └── errors/             # Доменные ошибки
│   │   │       └── UserErrors.ts
│   │   │
│   │   ├── application/            # Use Cases / Application Logic
│   │   │   ├── ports/              # Порты (интерфейсы)
│   │   │   │   ├── UserRepository.ts
│   │   │   │   └── UserNotifier.ts
│   │   │   ├── services/           # Application Services
│   │   │   │   └── UserService.ts
│   │   │   ├── commands/           # Command Handlers (CQRS)
│   │   │   │   ├── CreateUser.ts
│   │   │   │   └── UpdateUser.ts
│   │   │   └── queries/            # Query Handlers (CQRS)
│   │   │       ├── GetUserById.ts
│   │   │       └── ListUsers.ts
│   │   │
│   │   ├── infrastructure/         # Реализации портов
│   │   │   ├── UserRepositoryLive.ts
│   │   │   ├── UserRepositoryInMemory.ts
│   │   │   └── UserNotifierLive.ts
│   │   │
│   │   ├── adapters/               # Внешние интерфейсы
│   │   │   ├── http/
│   │   │   │   ├── UserRoutes.ts
│   │   │   │   └── UserHandlers.ts
│   │   │   └── rpc/
│   │   │       └── UserRpcs.ts
│   │   │
│   │   └── index.ts                # Public API модуля
│   │
│   ├── order-processing/
│   │   └── ...
│   │
│   └── payment-gateway/
│       └── ...

├── shared/                          # Общие утилиты
│   ├── kernel/                      # Shared Kernel (общие типы)
│   │   ├── branded-types.ts         # UserId, Email, etc.
│   │   ├── pagination.ts            # Paginated<T>
│   │   └── money.ts                 # Money value object
│   ├── errors/                      # Общие ошибки
│   │   └── infrastructure-errors.ts
│   ├── utils/                       # Чистые утилиты
│   │   ├── date.ts
│   │   └── string.ts
│   └── middleware/                   # Общие middleware
│       ├── AuthMiddleware.ts
│       ├── LoggingMiddleware.ts
│       └── CorsMiddleware.ts

├── infrastructure/                   # Глобальная инфраструктура
│   ├── database/
│   │   ├── DatabaseClient.ts        # Tag + interface
│   │   ├── DatabaseClientLive.ts    # Production
│   │   └── migrations/
│   ├── cache/
│   │   ├── CacheService.ts
│   │   └── CacheServiceRedis.ts
│   ├── messaging/
│   │   ├── EventBus.ts
│   │   └── EventBusLive.ts
│   └── observability/
│       ├── TracingLive.ts
│       ├── MetricsLive.ts
│       └── LoggingLive.ts

├── layers/                           # Композиция Layer
│   ├── AppLive.ts
│   ├── InfrastructureLive.ts
│   └── AppTest.ts

├── config/                           # Конфигурация
│   ├── AppConfig.ts
│   └── ConfigLive.ts

└── main.ts                           # Entry point

23. Generics

23.1 Правила параметров типов

ПараметрСемантикаГде используется
ASuccess typeEffect<A, E, R>, Option<A>
EError typeEffect<A, E, R>
RRequirements/ContextEffect<A, E, R>, Layer<A, E, R>
BВторой success typemap, flatMap
SelfРекурсивная ссылкаSchema.Class, tagged errors
IInput typeТрансформации
OOutput typeТрансформации
KKey typeMap<K, V>, Record<K, V>
VValue typeMap<K, V>, Record<K, V>
TПроизвольный типGeneric-утилиты (последний выбор)
SState typeReducer, Machine
// ✅ В реальном коде — описательные имена для сложных generic
const makeRepository = <
  Entity,
  CreateInput,
  EntityId,
  FindError
>(config: {
  readonly tableName: string
  readonly idField: keyof Entity
  readonly schema: Schema.Schema<Entity>
}): {
  readonly findById: (id: EntityId) => Effect.Effect<Entity, FindError>
  readonly create: (input: CreateInput) => Effect.Effect<Entity>
} => ...

// ✅ Effect-стиль — A, E, R для Effect-подобных типов
const retry = <A, E, R>(
  self: Effect.Effect<A, E, R>,
  schedule: Schedule.Schedule<unknown, E>
): Effect.Effect<A, E, R> => ...

24. Ref и состояние

24.1 Ref

// ✅ Ref-переменные — суффикс Ref
const counterRef: Ref.Ref<number> = yield* Ref.make(0)
const stateRef: Ref.Ref<AppState> = yield* Ref.make(initialState)
const usersRef: Ref.Ref<ReadonlyMap<UserId, User>> = yield* Ref.make(new Map())
const configRef: Ref.Ref<AppConfig> = yield* Ref.make(defaultConfig)

// ✅ SynchronizedRef — суффикс SyncRef
const cacheSyncRef: SynchronizedRef.SynchronizedRef<Cache> =
  yield* SynchronizedRef.make(emptyCache)

// ✅ FiberRef — описание контекста
const currentUserRef: FiberRef.FiberRef<Option.Option<User>> =
  FiberRef.unsafeMake(Option.none())

const requestIdRef: FiberRef.FiberRef<string> =
  FiberRef.unsafeMake("unknown")

const logContextRef: FiberRef.FiberRef<ReadonlyRecord<string, string>> =
  FiberRef.unsafeMake({})

24.2 Операции с Ref

// ✅ Функции работы с Ref — стандартные глаголы
const incrementCounter = Ref.update(counterRef, (n) => n + 1)
const getCounter = Ref.get(counterRef)
const resetCounter = Ref.set(counterRef, 0)

const addUser = (user: User) =>
  Ref.update(usersRef, (map) =>
    ReadonlyMap.set(map, user.id, user)
  )

25. Resource Management

25.1 Scope и Finalizer

// ✅ Scoped ресурсы — acquire/release глаголы
const acquireDatabaseConnection = Effect.acquireRelease(
  connectToDatabase(config),           // acquire
  (connection) => connection.close()   // release
)

const acquireFileHandle = (path: string) =>
  Effect.acquireRelease(
    openFile(path),
    (handle) => closeFile(handle)
  )

// ✅ Scoped-сервисы — суффикс Scoped
const makeScopedHttpClient = Effect.acquireRelease(
  createHttpClient(config),
  (client) => client.shutdown()
)

// ✅ Pool — суффикс Pool
const connectionPool = Pool.make({
  acquire: connectToDatabase(config),
  size: 10,
})
type ConnectionPool = Pool.Pool<DatabaseConnection, DatabaseConnectionError>

26. Schedule, Duration, Cron

26.1 Schedule

// ✅ Schedule-переменные — описание политики
const retryPolicy = pipe(
  Schedule.exponential("100 millis"),
  Schedule.intersect(Schedule.recurs(5)),
  Schedule.jittered
)

const pollingSchedule = Schedule.fixed("5 seconds")

const backoffSchedule = pipe(
  Schedule.exponential("1 second", 2),
  Schedule.either(Schedule.spaced("30 seconds")),
)

const healthCheckSchedule = Schedule.fixed("30 seconds")
const cacheRefreshSchedule = Schedule.fixed("5 minutes")
const metricsFlushSchedule = Schedule.fixed("10 seconds")
const tokenRotationSchedule = Schedule.fixed("1 hour")

26.2 Duration

// ✅ Duration-переменные — описание назначения
const requestTimeout = Duration.seconds(30)
const connectionTimeout = Duration.seconds(5)
const idleTimeout = Duration.minutes(15)
const sessionTtl = Duration.hours(24)
const cacheTtl = Duration.minutes(5)
const lockTimeout = Duration.seconds(10)
const gracefulShutdownTimeout = Duration.seconds(30)

27. Метрики и трейсинг

27.1 Метрики

// ✅ Паттерн: {module}.{metric_type}.{name} (snake_case через точки)
const httpRequestDuration = Metric.histogram("http.request.duration_ms", {
  description: "HTTP request duration in milliseconds",
  boundaries: [10, 25, 50, 100, 250, 500, 1000, 2500, 5000],
})

const httpRequestTotal = Metric.counter("http.request.total", {
  description: "Total number of HTTP requests",
})

const activeConnectionsGauge = Metric.gauge("db.connections.active", {
  description: "Number of active database connections",
})

const orderAmountSummary = Metric.summary("order.amount_usd", {
  description: "Order amounts in USD",
  maxAge: Duration.hours(1),
  quantiles: [0.5, 0.9, 0.95, 0.99],
})

const cacheHitRate = Metric.counter("cache.hit.total")
const cacheMissRate = Metric.counter("cache.miss.total")

27.2 Spans / Tracing

// ✅ Span-имена — {module}.{operation} (точечная нотация)
const findUser = (id: UserId) =>
  pipe(
    doFindUser(id),
    Effect.withSpan("user.findById", {
      attributes: { "user.id": id },
    })
  )

const processPayment = (orderId: OrderId) =>
  pipe(
    doProcessPayment(orderId),
    Effect.withSpan("payment.process", {
      attributes: { "order.id": orderId },
    })
  )

// ✅ Атрибуты — {domain}.{attribute} (точечная нотация, snake_case)
// user.id, user.email, order.id, order.status
// http.method, http.url, http.status_code
// db.statement, db.duration_ms

28. Anti-patterns: запрещённые практики

28.1 Именование

// ❌ ЗАПРЕЩЕНО: Manager/Handler/Processor/Helper без контекста
class UserManager { }           // ❌ — что именно "управляет"?
class DataHandler { }           // ❌ — какие данные?
class RequestProcessor { }      // ❌ — что обрабатывает?
class StringHelper { }          // ❌ — используй описательные имена функций

// ✅ ПРАВИЛЬНО: конкретные имена
class UserAuthenticationService { }
class OrderFulfillmentService { }
class EmailDeliveryService { }

// ❌ ЗАПРЕЩЕНО: абстрактные имена
const data = ...               // ❌
const info = ...               // ❌
const temp = ...               // ❌
const result = ...             // ❌ (допустимо только внутри gen)
const value = ...              // ❌
const item = ...               // ❌
const stuff = ...              // ❌

// ❌ ЗАПРЕЩЕНО: отрицательные имена для булевых
const isNotActive = false      // ❌ — двойное отрицание при проверке
const hasNoPermission = true   // ❌
const cannotEdit = false       // ❌

// ✅ ПРАВИЛЬНО: положительные имена
const isActive = true          // if (isActive)
const hasPermission = false    // if (!hasPermission)
const isEditable = true        // if (isEditable)

// ❌ ЗАПРЕЩЕНО: глагол для типа
type CreateUser = { ... }      // ⚠️ допустимо только для Command/Request
type FetchOrder = { ... }      // ❌ — тип не действие

// ✅ ПРАВИЛЬНО
type User = { ... }
type CreateUserCommand = { ... }
type FetchOrderQuery = { ... }

// ❌ ЗАПРЕЩЕНО: одна буква (кроме лямбд и generic)
const u = getUser()            // ❌
const o = createOrder()        // ❌

// ✅ ПРАВИЛЬНО
const user = yield* getUser()
const order = yield* createOrder()

// ❌ ЗАПРЕЩЕНО: тавтология
type UserUser = { ... }                   // ❌
const getUserUser = () => ...             // ❌
const userServiceService = () => ...      // ❌

// ❌ ЗАПРЕЩЕНО: слово "Impl" для реализации
class UserServiceImpl { }                 // ❌ — используй Live/InMemory/Postgres
const UserRepositoryImpl = Layer.effect(  // ❌

28.2 Структурные Anti-patterns

// ❌ ЗАПРЕЩЕНО: utils.ts / helpers.ts — файл-свалка
// src/utils.ts — ❌ всё в одном файле

// ✅ ПРАВИЛЬНО: по доменам
// src/shared/utils/date.ts
// src/shared/utils/string.ts
// src/shared/utils/crypto.ts

// ❌ ЗАПРЕЩЕНО: God-сервис (>10 методов)
interface UserService {
  findById: ...
  create: ...
  update: ...
  delete: ...
  sendEmail: ...           // ❌ — не ответственность UserService
  generateReport: ...      // ❌ — отдельный ReportService
  syncWithCRM: ...         // ❌ — отдельный CrmSyncService
  processPayment: ...      // ❌ — отдельный PaymentService
}

// ✅ ПРАВИЛЬНО: Single Responsibility
interface UserService { findById, create, update, delete, list }
interface EmailService { send, sendBatch, verify }
interface ReportService { generate, schedule }
interface CrmSyncService { sync, syncBatch }

29. Краткая шпаргалка

Мгновенная справка

ПЕРЕМЕННАЯ          camelCase           userId, activeUsers, maxRetries
БУЛЕВА              is/has/can + Noun   isActive, hasRole, canEdit
ФУНКЦИЯ ЧИСТАЯ      verb + Noun         createUser, formatDate, calcTotal
ФУНКЦИЯ EFFECT      verb + Noun         findUserById, saveOrder, sendEmail
ПРЕДИКАТ            is/has/can          isValid, hasPermission, canAccess
КОНСТРУКТОР         make + Noun         makeUser, makeDefaultConfig
HOF                 with + Noun         withRetry, withTimeout, withLogging
ФАБРИКА             make + Noun         makeValidator, makeRepository

ТИП                 PascalCase          User, OrderSummary, PaymentResult
SCHEMA              PascalCase          CreateUserRequest, UserResponse
ОШИБКА              PascalCase + Error  UserNotFoundError, PaymentDeclinedError
UNION ОШИБОК        PascalCase + Error  UserModuleError, AppError
BRAND               PascalCase строка   "UserId", "Email", "Port"
DISCRIMINATED UNION PascalCase _tag     { _tag: "UserCreated" }
GENERIC             A, E, R, B, K, V   Effect<A, E, R>

СЕРВИС (Tag)        PascalCase          UserService, CacheService
РЕПОЗИТОРИЙ         PascalCase          UserRepository, OrderRepository
LAYER (prod)        PascalCase + Live   UserServiceLive, AppLive
LAYER (test)        + InMemory/Test     UserRepositoryInMemory, AppTest
MIDDLEWARE          PascalCase + MW     AuthMiddleware, LoggingMiddleware

ФАЙЛ (модуль)       PascalCase.ts       UserService.ts, UserErrors.ts
ФАЙЛ (утилита)      camelCase.ts        formatDate.ts, hashPassword.ts
ДИРЕКТОРИЯ          kebab-case          user-management/, shared/
ТЕСТ                PascalCase.test.ts  UserService.test.ts

REF                 name + Ref          counterRef, stateRef, usersRef
FIBER               name + Fiber        workerFiber, healthCheckFiber
STREAM              name + Stream       logStream, priceUpdateStream
QUEUE               name + Queue        taskQueue, deadLetterQueue
SCHEDULE            описательное        retryPolicy, pollingSchedule
DURATION            описательное        requestTimeout, cacheTtl
МЕТРИКА             dot.notation        http.request.duration_ms
SPAN                dot.notation        user.findById, payment.process
ENV VARIABLE        UPPER_SNAKE         DB_HOST, AUTH_JWT_SECRET
CONFIG              PascalCase + Config DatabaseConfig, HttpServerConfig

Правило №1: Если сомневаешься в имени — представь, что через 6 месяцев ты открыл этот файл в 3 часа ночи во время инцидента. Имя должно быть настолько очевидным, что не требует ни контекста, ни документации.

Правило №2: Консистентность важнее креативности. Лучше скучный, но предсказуемый findUserById, чем элегантный, но неочевидный resolveUser.

Правило №3: Effect-тип — это документация. Effect<User, UserNotFoundError, UserRepository> рассказывает всю историю: что возвращает, как может упасть, что требует. Имя функции лишь дополняет эту историю.