{"id":"9e79d68b-c9ab-49b9-872d-eb48b7807d37","shortId":"bR9Qfb","kind":"skill","title":"fp-backend","tagline":"Functional programming patterns for Node.js/Deno backend development using fp-ts, ReaderTaskEither, and functional dependency injection","description":"# fp-ts Backend Patterns\n\nFunctional programming patterns for building type-safe, testable backend services using fp-ts.\n\n## When to Use\n- You are building or refactoring a Node.js or Deno backend with fp-ts.\n- The task involves dependency injection, service composition, or typed backend errors with `ReaderTaskEither`.\n- You need functional backend architecture patterns rather than isolated utility snippets.\n\n## Core Concepts\n\n### ReaderTaskEither (RTE)\n\nThe `ReaderTaskEither<R, E, A>` type is the backbone of functional backend development:\n- **R** (Reader): Dependencies/environment (database, config, logger)\n- **E** (Either left): Error type\n- **A** (Either right): Success value\n\n```typescript\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport * as TE from 'fp-ts/TaskEither'\nimport { pipe } from 'fp-ts/function'\n\n// Define your dependencies\ntype Deps = {\n  db: DatabaseClient\n  logger: Logger\n  config: Config\n}\n\n// Define domain errors\ntype AppError =\n  | { _tag: 'NotFound'; resource: string; id: string }\n  | { _tag: 'ValidationError'; message: string }\n  | { _tag: 'DatabaseError'; cause: unknown }\n  | { _tag: 'Unauthorized'; reason: string }\n\n// A service function\nconst getUser = (id: string): RTE.ReaderTaskEither<Deps, AppError, User> =>\n  pipe(\n    RTE.ask<Deps>(),\n    RTE.flatMap(({ db, logger }) =>\n      pipe(\n        RTE.fromTaskEither(db.users.findById(id)),\n        RTE.mapLeft((e): AppError => ({ _tag: 'DatabaseError', cause: e })),\n        RTE.flatMap(user =>\n          user\n            ? RTE.right(user)\n            : RTE.left({ _tag: 'NotFound', resource: 'User', id })\n        ),\n        RTE.tap(user => RTE.fromIO(() => logger.info(`Found user: ${user.id}`)))\n      )\n    )\n  )\n```\n\n## Service Layer Patterns\n\n### Defining Service Modules\n\nStructure services as modules exporting RTE functions:\n\n```typescript\n// src/services/user.service.ts\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as A from 'fp-ts/Array'\nimport { pipe } from 'fp-ts/function'\n\ntype UserDeps = {\n  db: DatabaseClient\n  hasher: PasswordHasher\n  mailer: EmailService\n}\n\ntype UserError =\n  | { _tag: 'UserNotFound'; id: string }\n  | { _tag: 'EmailExists'; email: string }\n  | { _tag: 'InvalidPassword' }\n\n// Create user\nexport const create = (\n  input: CreateUserInput\n): RTE.ReaderTaskEither<UserDeps, UserError, User> =>\n  pipe(\n    RTE.ask<UserDeps>(),\n    RTE.flatMap(({ db, hasher }) =>\n      pipe(\n        // Check email uniqueness\n        checkEmailUnique(input.email),\n        RTE.flatMap(() =>\n          RTE.fromTaskEither(hasher.hash(input.password))\n        ),\n        RTE.flatMap(hashedPassword =>\n          RTE.fromTaskEither(\n            db.users.create({\n              ...input,\n              password: hashedPassword,\n            })\n          )\n        )\n      )\n    )\n  )\n\n// Find by ID\nexport const findById = (\n  id: string\n): RTE.ReaderTaskEither<UserDeps, UserError, User> =>\n  pipe(\n    RTE.ask<UserDeps>(),\n    RTE.flatMap(({ db }) =>\n      pipe(\n        RTE.fromTaskEither(db.users.findUnique({ where: { id } })),\n        RTE.flatMap(user =>\n          user\n            ? RTE.right(user)\n            : RTE.left({ _tag: 'UserNotFound' as const, id })\n        )\n      )\n    )\n  )\n\n// Find many with pagination\nexport const findMany = (\n  params: PaginationParams\n): RTE.ReaderTaskEither<UserDeps, UserError, PaginatedResult<User>> =>\n  pipe(\n    RTE.ask<UserDeps>(),\n    RTE.flatMap(({ db }) =>\n      RTE.fromTaskEither(\n        pipe(\n          TE.Do,\n          TE.bind('users', () => db.users.findMany({\n            skip: params.offset,\n            take: params.limit,\n          })),\n          TE.bind('total', () => db.users.count()),\n          TE.map(({ users, total }) => ({\n            data: users,\n            total,\n            ...params,\n          }))\n        )\n      )\n    )\n  )\n\nconst checkEmailUnique = (\n  email: string\n): RTE.ReaderTaskEither<UserDeps, UserError, void> =>\n  pipe(\n    RTE.ask<UserDeps>(),\n    RTE.flatMap(({ db }) =>\n      pipe(\n        RTE.fromTaskEither(db.users.findUnique({ where: { email } })),\n        RTE.flatMap(existing =>\n          existing\n            ? RTE.left({ _tag: 'EmailExists' as const, email })\n            : RTE.right(undefined)\n        )\n      )\n    )\n  )\n```\n\n### Composing Services\n\n```typescript\n// src/services/order.service.ts\nimport * as UserService from './user.service'\nimport * as ProductService from './product.service'\nimport * as PaymentService from './payment.service'\n\ntype OrderDeps = UserService.UserDeps &\n  ProductService.ProductDeps &\n  PaymentService.PaymentDeps & {\n    db: DatabaseClient\n  }\n\nexport const createOrder = (\n  userId: string,\n  items: OrderItem[]\n): RTE.ReaderTaskEither<OrderDeps, OrderError, Order> =>\n  pipe(\n    RTE.Do,\n    // Validate user exists\n    RTE.bind('user', () =>\n      pipe(\n        UserService.findById(userId),\n        RTE.mapLeft(toOrderError)\n      )\n    ),\n    // Validate and get products\n    RTE.bind('products', () =>\n      pipe(\n        items,\n        A.traverse(RTE.ApplicativePar)(item =>\n          ProductService.findById(item.productId)\n        ),\n        RTE.mapLeft(toOrderError)\n      )\n    ),\n    // Calculate total\n    RTE.bind('total', ({ products }) =>\n      RTE.right(calculateTotal(products, items))\n    ),\n    // Process payment\n    RTE.bind('payment', ({ user, total }) =>\n      pipe(\n        PaymentService.charge(user, total),\n        RTE.mapLeft(toOrderError)\n      )\n    ),\n    // Create order\n    RTE.flatMap(({ user, products, total, payment }) =>\n      createOrderRecord(user, products, items, total, payment)\n    )\n  )\n```\n\n## Functional Dependency Injection\n\n### Building the Dependency Container\n\n```typescript\n// src/deps.ts\nimport { pipe } from 'fp-ts/function'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as RTE from 'fp-ts/ReaderTaskEither'\n\n// Layer 0: Config (no dependencies)\ntype Config = {\n  database: { url: string; poolSize: number }\n  redis: { url: string }\n  jwt: { secret: string; expiresIn: string }\n}\n\nconst loadConfig = (): TE.TaskEither<Error, Config> =>\n  TE.tryCatch(\n    async () => ({\n      database: {\n        url: process.env.DATABASE_URL!,\n        poolSize: parseInt(process.env.DB_POOL_SIZE || '10'),\n      },\n      redis: { url: process.env.REDIS_URL! },\n      jwt: {\n        secret: process.env.JWT_SECRET!,\n        expiresIn: process.env.JWT_EXPIRES || '1d',\n      },\n    }),\n    (e) => new Error(`Config error: ${e}`)\n  )\n\n// Layer 1: Infrastructure (depends on config)\ntype Infrastructure = {\n  config: Config\n  db: PrismaClient\n  redis: RedisClient\n  logger: Logger\n}\n\nconst buildInfrastructure = (\n  config: Config\n): TE.TaskEither<Error, Infrastructure> =>\n  pipe(\n    TE.Do,\n    TE.bind('db', () =>\n      TE.tryCatch(\n        async () => {\n          const prisma = new PrismaClient({\n            datasources: { db: { url: config.database.url } },\n          })\n          await prisma.$connect()\n          return prisma\n        },\n        (e) => new Error(`Database error: ${e}`)\n      )\n    ),\n    TE.bind('redis', () =>\n      TE.tryCatch(\n        async () => createRedisClient(config.redis.url),\n        (e) => new Error(`Redis error: ${e}`)\n      )\n    ),\n    TE.bind('logger', () => TE.right(createLogger())),\n    TE.map(({ db, redis, logger }) => ({\n      config,\n      db,\n      redis,\n      logger,\n    }))\n  )\n\n// Layer 2: Services (depends on infrastructure)\ntype Services = {\n  hasher: PasswordHasher\n  jwt: JwtService\n  mailer: EmailService\n}\n\nconst buildServices = (infra: Infrastructure): Services => ({\n  hasher: createBcryptHasher(),\n  jwt: createJwtService(infra.config.jwt),\n  mailer: createEmailService(infra.config),\n})\n\n// Full application dependencies\nexport type AppDeps = Infrastructure & Services\n\nexport const buildDeps = (): TE.TaskEither<Error, AppDeps> =>\n  pipe(\n    loadConfig(),\n    TE.flatMap(buildInfrastructure),\n    TE.map(infra => ({\n      ...infra,\n      ...buildServices(infra),\n    }))\n  )\n\n// Cleanup\nexport const destroyDeps = (deps: AppDeps): TE.TaskEither<Error, void> =>\n  pipe(\n    TE.tryCatch(\n      async () => {\n        await deps.db.$disconnect()\n        await deps.redis.quit()\n      },\n      (e) => new Error(`Cleanup error: ${e}`)\n    )\n  )\n```\n\n### Running Programs with Dependencies\n\n```typescript\n// src/main.ts\nimport { pipe } from 'fp-ts/function'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as RTE from 'fp-ts/ReaderTaskEither'\n\nconst program: RTE.ReaderTaskEither<AppDeps, AppError, void> = pipe(\n  RTE.ask<AppDeps>(),\n  RTE.flatMap(deps =>\n    pipe(\n      startServer(deps),\n      RTE.fromTaskEither\n    )\n  )\n)\n\nconst main = async () => {\n  const result = await pipe(\n    buildDeps(),\n    TE.mapLeft((e): AppError => ({ _tag: 'StartupError', cause: e })),\n    TE.flatMap(deps =>\n      pipe(\n        program(deps),\n        TE.tap(() => TE.fromIO(() => console.log('Server running'))),\n        // Cleanup on exit\n        TE.tapError(() => destroyDeps(deps))\n      )\n    )\n  )()\n\n  if (result._tag === 'Left') {\n    console.error('Failed to start:', result.left)\n    process.exit(1)\n  }\n}\n\nmain()\n```\n\n## Database Operations\n\n### Prisma Wrappers\n\n```typescript\n// src/lib/db.ts\nimport * as TE from 'fp-ts/TaskEither'\nimport * as O from 'fp-ts/Option'\nimport { PrismaClient, Prisma } from '@prisma/client'\n\ntype DbError =\n  | { _tag: 'RecordNotFound'; model: string; id: string }\n  | { _tag: 'UniqueViolation'; field: string }\n  | { _tag: 'ForeignKeyViolation'; field: string }\n  | { _tag: 'UnknownDbError'; cause: unknown }\n\n// Wrap Prisma operations\nconst wrapPrisma = <A>(\n  operation: () => Promise<A>\n): TE.TaskEither<DbError, A> =>\n  TE.tryCatch(operation, (error): DbError => {\n    if (error instanceof Prisma.PrismaClientKnownRequestError) {\n      switch (error.code) {\n        case 'P2002':\n          return {\n            _tag: 'UniqueViolation',\n            field: (error.meta?.target as string[])?.join(', ') || 'unknown',\n          }\n        case 'P2003':\n          return {\n            _tag: 'ForeignKeyViolation',\n            field: error.meta?.field_name as string || 'unknown',\n          }\n        case 'P2025':\n          return {\n            _tag: 'RecordNotFound',\n            model: error.meta?.modelName as string || 'unknown',\n            id: 'unknown',\n          }\n      }\n    }\n    return { _tag: 'UnknownDbError', cause: error }\n  })\n\n// Repository factory\nexport const createRepository = <\n  Model,\n  CreateInput,\n  UpdateInput,\n  WhereUnique,\n  WhereMany\n>(\n  db: PrismaClient,\n  delegate: {\n    findUnique: (args: { where: WhereUnique }) => Promise<Model | null>\n    findMany: (args: { where?: WhereMany; skip?: number; take?: number }) => Promise<Model[]>\n    create: (args: { data: CreateInput }) => Promise<Model>\n    update: (args: { where: WhereUnique; data: UpdateInput }) => Promise<Model>\n    delete: (args: { where: WhereUnique }) => Promise<Model>\n    count: (args?: { where?: WhereMany }) => Promise<number>\n  }\n) => ({\n  findUnique: (where: WhereUnique): TE.TaskEither<DbError, O.Option<Model>> =>\n    pipe(\n      wrapPrisma(() => delegate.findUnique({ where })),\n      TE.map(O.fromNullable)\n    ),\n\n  findMany: (\n    where?: WhereMany,\n    pagination?: { skip: number; take: number }\n  ): TE.TaskEither<DbError, Model[]> =>\n    wrapPrisma(() => delegate.findMany({ where, ...pagination })),\n\n  create: (data: CreateInput): TE.TaskEither<DbError, Model> =>\n    wrapPrisma(() => delegate.create({ data })),\n\n  update: (\n    where: WhereUnique,\n    data: UpdateInput\n  ): TE.TaskEither<DbError, Model> =>\n    wrapPrisma(() => delegate.update({ where, data })),\n\n  delete: (where: WhereUnique): TE.TaskEither<DbError, Model> =>\n    wrapPrisma(() => delegate.delete({ where })),\n\n  count: (where?: WhereMany): TE.TaskEither<DbError, number> =>\n    wrapPrisma(() => delegate.count({ where })),\n})\n\n// Usage\nconst userRepo = createRepository(prisma, prisma.user)\n```\n\n### Transaction Handling\n\n```typescript\n// src/lib/transaction.ts\nimport * as TE from 'fp-ts/TaskEither'\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport { PrismaClient } from '@prisma/client'\nimport { pipe } from 'fp-ts/function'\n\ntype TxClient = Omit<\n  PrismaClient,\n  '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'\n>\n\ntype TxDeps = { tx: TxClient }\n\n// Transaction wrapper\nexport const withTransaction = <R extends { db: PrismaClient }, E, A>(\n  program: RTE.ReaderTaskEither<R & TxDeps, E, A>\n): RTE.ReaderTaskEither<R, E | DbError, A> =>\n  pipe(\n    RTE.ask<R>(),\n    RTE.flatMap(deps =>\n      RTE.fromTaskEither(\n        TE.tryCatch(\n          () =>\n            deps.db.$transaction(async tx => {\n              const result = await program({ ...deps, tx })()\n              if (result._tag === 'Left') {\n                throw result.left // Rollback\n              }\n              return result.right\n            }),\n          (error): E | DbError => {\n            // Re-throw domain errors\n            if (typeof error === 'object' && error !== null && '_tag' in error) {\n              return error as E\n            }\n            return { _tag: 'UnknownDbError', cause: error }\n          }\n        )\n      )\n    )\n  )\n\n// Usage in service\nexport const transferFunds = (\n  fromId: string,\n  toId: string,\n  amount: number\n): RTE.ReaderTaskEither<AppDeps, TransferError, Transfer> =>\n  withTransaction(\n    pipe(\n      RTE.Do,\n      RTE.bind('from', () => debitAccount(fromId, amount)),\n      RTE.bind('to', () => creditAccount(toId, amount)),\n      RTE.bind('transfer', ({ from, to }) =>\n        createTransferRecord(from, to, amount)\n      ),\n      RTE.map(({ transfer }) => transfer)\n    )\n  )\n\n// Inside transaction, use tx instead of db\nconst debitAccount = (\n  accountId: string,\n  amount: number\n): RTE.ReaderTaskEither<TxDeps, TransferError, Account> =>\n  pipe(\n    RTE.ask<TxDeps>(),\n    RTE.flatMap(({ tx }) =>\n      RTE.fromTaskEither(\n        pipe(\n          TE.tryCatch(\n            () =>\n              tx.account.update({\n                where: { id: accountId },\n                data: { balance: { decrement: amount } },\n              }),\n            toDbError\n          ),\n          TE.flatMap(account =>\n            account.balance < 0\n              ? TE.left({ _tag: 'InsufficientFunds' as const, accountId })\n              : TE.right(account)\n          )\n        )\n      )\n    )\n  )\n```\n\n## Middleware Patterns\n\n### Express Middleware\n\n```typescript\n// src/middleware/fp-express.ts\nimport { Request, Response, NextFunction, RequestHandler } from 'express'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// Convert RTE handler to Express middleware\nexport const toHandler =\n  <R, E, A>(\n    getDeps: (req: Request) => R,\n    handler: (req: Request) => RTE.ReaderTaskEither<R, E, A>,\n    onError: (error: E, res: Response) => void\n  ): RequestHandler =>\n  async (req, res, next) => {\n    const deps = getDeps(req)\n    const result = await handler(req)(deps)()\n\n    pipe(\n      result,\n      E.fold(\n        error => onError(error, res),\n        data => res.json(data)\n      )\n    )\n  }\n\n// Error handler\nconst handleError = (error: AppError, res: Response): void => {\n  switch (error._tag) {\n    case 'NotFound':\n      res.status(404).json({ error: error.resource + ' not found' })\n      break\n    case 'ValidationError':\n      res.status(400).json({ error: error.message })\n      break\n    case 'Unauthorized':\n      res.status(401).json({ error: error.reason })\n      break\n    default:\n      res.status(500).json({ error: 'Internal server error' })\n  }\n}\n\n// Usage\nconst getUserHandler = toHandler(\n  req => req.app.locals.deps as AppDeps,\n  req => UserService.findById(req.params.id),\n  handleError\n)\n\napp.get('/users/:id', getUserHandler)\n```\n\n### Hono Middleware\n\n```typescript\n// src/middleware/fp-hono.ts\nimport { Hono, Context, MiddlewareHandler } from 'hono'\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\n\n// Store deps in context\ndeclare module 'hono' {\n  interface ContextVariableMap {\n    deps: AppDeps\n  }\n}\n\n// Dependency injection middleware\nexport const withDeps = (deps: AppDeps): MiddlewareHandler =>\n  async (c, next) => {\n    c.set('deps', deps)\n    await next()\n  }\n\n// Convert RTE to Hono handler\nexport const toHonoHandler =\n  <E, A>(\n    handler: (c: Context) => RTE.ReaderTaskEither<AppDeps, E, A>,\n    onError: (error: E, c: Context) => Response\n  ) =>\n  async (c: Context): Promise<Response> => {\n    const deps = c.get('deps')\n    const result = await handler(c)(deps)()\n\n    return pipe(\n      result,\n      E.fold(\n        error => onError(error, c),\n        data => c.json(data)\n      )\n    )\n  }\n\n// Validation middleware\nexport const validate =\n  <T>(schema: z.ZodSchema<T>): MiddlewareHandler =>\n  async (c, next) => {\n    const body = await c.req.json()\n    const result = schema.safeParse(body)\n\n    if (!result.success) {\n      return c.json(\n        { error: 'Validation failed', details: result.error.flatten() },\n        400\n      )\n    }\n\n    c.set('validatedBody', result.data)\n    await next()\n  }\n\n// Auth middleware using RTE\nexport const requireAuth: MiddlewareHandler = async (c, next) => {\n  const deps = c.get('deps')\n  const token = c.req.header('Authorization')?.replace('Bearer ', '')\n\n  if (!token) {\n    return c.json({ error: 'No token provided' }, 401)\n  }\n\n  const result = await pipe(\n    deps.jwt.verify(token),\n    TE.mapLeft(() => ({ _tag: 'Unauthorized' as const, reason: 'Invalid token' }))\n  )()\n\n  if (E.isLeft(result)) {\n    return c.json({ error: result.left.reason }, 401)\n  }\n\n  c.set('user', result.right)\n  await next()\n}\n\n// Usage\nconst app = new Hono()\n\napp.use('*', withDeps(deps))\napp.use('/api/*', requireAuth)\n\napp.get(\n  '/api/users/:id',\n  toHonoHandler(\n    c => UserService.findById(c.req.param('id')),\n    (error, c) => {\n      if (error._tag === 'UserNotFound') {\n        return c.json({ error: 'User not found' }, 404)\n      }\n      return c.json({ error: 'Internal error' }, 500)\n    }\n  )\n)\n```\n\n### Request Context Pattern\n\n```typescript\n// src/context.ts\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport { pipe } from 'fp-ts/function'\n\n// Request-scoped context\ntype RequestContext = {\n  requestId: string\n  userId: O.Option<string>\n  startTime: number\n}\n\ntype ContextDeps = AppDeps & { ctx: RequestContext }\n\n// Logging with context\nconst logWithContext =\n  (level: 'info' | 'warn' | 'error') =>\n  (message: string, meta?: object): RTE.ReaderTaskEither<ContextDeps, never, void> =>\n    pipe(\n      RTE.ask<ContextDeps>(),\n      RTE.flatMap(({ logger, ctx }) =>\n        RTE.fromIO(() =>\n          loggerlevel,\n            elapsed: Date.now() - ctx.startTime,\n          })\n        )\n      )\n    )\n\nexport const log = {\n  info: logWithContext('info'),\n  warn: logWithContext('warn'),\n  error: logWithContext('error'),\n}\n\n// Middleware to create context\nexport const withContext: MiddlewareHandler = async (c, next) => {\n  const deps = c.get('deps')\n  const ctx: RequestContext = {\n    requestId: crypto.randomUUID(),\n    userId: O.fromNullable(c.get('user')?.id),\n    startTime: Date.now(),\n  }\n\n  c.set('deps', { ...deps, ctx })\n\n  // Log request start\n  deps.logger.info('Request started', {\n    requestId: ctx.requestId,\n    method: c.req.method,\n    path: c.req.path,\n  })\n\n  await next()\n\n  // Log request end\n  deps.logger.info('Request completed', {\n    requestId: ctx.requestId,\n    status: c.res.status,\n    elapsed: Date.now() - ctx.startTime,\n  })\n}\n```\n\n## Error Handling Patterns\n\n### Typed Error Hierarchy\n\n```typescript\n// src/errors.ts\nimport * as E from 'fp-ts/Either'\nimport * as O from 'fp-ts/Option'\n\n// Base error types\ntype DomainError =\n  | NotFoundError\n  | ValidationError\n  | ConflictError\n  | AuthError\n  | InfrastructureError\n\ntype NotFoundError = {\n  _tag: 'NotFoundError'\n  resource: string\n  id: string\n}\n\ntype ValidationError = {\n  _tag: 'ValidationError'\n  field: string\n  message: string\n  value?: unknown\n}\n\ntype ConflictError = {\n  _tag: 'ConflictError'\n  resource: string\n  field: string\n  value: string\n}\n\ntype AuthError =\n  | { _tag: 'Unauthenticated' }\n  | { _tag: 'Unauthorized'; required: string }\n  | { _tag: 'TokenExpired' }\n\ntype InfrastructureError = {\n  _tag: 'InfrastructureError'\n  service: string\n  cause: unknown\n}\n\n// Smart constructors\nexport const notFound = (resource: string, id: string): NotFoundError => ({\n  _tag: 'NotFoundError',\n  resource,\n  id,\n})\n\nexport const validation = (\n  field: string,\n  message: string,\n  value?: unknown\n): ValidationError => ({\n  _tag: 'ValidationError',\n  field,\n  message,\n  value,\n})\n\nexport const conflict = (\n  resource: string,\n  field: string,\n  value: string\n): ConflictError => ({\n  _tag: 'ConflictError',\n  resource,\n  field,\n  value,\n})\n\n// Error to HTTP status mapping\nexport const toHttpStatus = (error: DomainError): number => {\n  switch (error._tag) {\n    case 'NotFoundError':\n      return 404\n    case 'ValidationError':\n      return 400\n    case 'ConflictError':\n      return 409\n    case 'Unauthenticated':\n      return 401\n    case 'Unauthorized':\n      return 403\n    case 'TokenExpired':\n      return 401\n    case 'InfrastructureError':\n      return 503\n    default:\n      return 500\n  }\n}\n\n// Error to response body\nexport const toResponseBody = (\n  error: DomainError\n): { error: string; details?: unknown } => {\n  switch (error._tag) {\n    case 'NotFoundError':\n      return { error: `${error.resource} not found` }\n    case 'ValidationError':\n      return {\n        error: 'Validation failed',\n        details: { field: error.field, message: error.message },\n      }\n    case 'ConflictError':\n      return {\n        error: `${error.resource} with ${error.field} already exists`,\n      }\n    case 'Unauthenticated':\n      return { error: 'Authentication required' }\n    case 'Unauthorized':\n      return { error: `Permission denied: ${error.required}` }\n    case 'TokenExpired':\n      return { error: 'Token expired' }\n    case 'InfrastructureError':\n      return { error: 'Service temporarily unavailable' }\n  }\n}\n```\n\n### Error Recovery\n\n```typescript\n// src/lib/recovery.ts\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport * as TE from 'fp-ts/TaskEither'\nimport { pipe } from 'fp-ts/function'\n\n// Retry with exponential backoff\nexport const withRetry =\n  <R, E, A>(\n    maxAttempts: number,\n    baseDelayMs: number,\n    shouldRetry: (error: E) => boolean\n  ) =>\n  (\n    operation: RTE.ReaderTaskEither<R, E, A>\n  ): RTE.ReaderTaskEither<R, E, A> =>\n    pipe(\n      RTE.ask<R>(),\n      RTE.flatMap(deps => {\n        const attempt = (\n          remaining: number,\n          delay: number\n        ): TE.TaskEither<E, A> =>\n          pipe(\n            operation(deps),\n            TE.orElse(error => {\n              if (remaining <= 0 || !shouldRetry(error)) {\n                return TE.left(error)\n              }\n              return pipe(\n                TE.fromTask(() => new Promise(r => setTimeout(r, delay))),\n                TE.flatMap(() => attempt(remaining - 1, delay * 2))\n              )\n            })\n          )\n\n        return RTE.fromTaskEither(attempt(maxAttempts - 1, baseDelayMs))\n      })\n    )\n\n// Fallback to cached value\nexport const withFallback =\n  <R extends { cache: CacheClient }, E, A>(\n    cacheKey: string,\n    ttlSeconds: number\n  ) =>\n  (\n    operation: RTE.ReaderTaskEither<R, E, A>\n  ): RTE.ReaderTaskEither<R, E, A> =>\n    pipe(\n      RTE.ask<R>(),\n      RTE.flatMap(({ cache, ...rest }) =>\n        pipe(\n          operation,\n          // On success, cache the result\n          RTE.tap(result =>\n            RTE.fromTaskEither(cache.set(cacheKey, result, ttlSeconds))\n          ),\n          // On failure, try to get cached value\n          RTE.orElse(error =>\n            pipe(\n              RTE.fromTaskEither(cache.get<A>(cacheKey)),\n              RTE.flatMap(cached =>\n                cached ? RTE.right(cached) : RTE.left(error)\n              )\n            )\n          )\n        )\n      )\n    )\n\n// Circuit breaker\ntype CircuitState = 'closed' | 'open' | 'half-open'\n\nexport const createCircuitBreaker = <E>(\n  failureThreshold: number,\n  resetTimeoutMs: number,\n  isFailure: (error: E) => boolean\n) => {\n  let state: CircuitState = 'closed'\n  let failures = 0\n  let lastFailure = 0\n\n  return <R, A>(\n    operation: RTE.ReaderTaskEither<R, E, A>\n  ): RTE.ReaderTaskEither<R, E | { _tag: 'CircuitOpen' }, A> =>\n    pipe(\n      RTE.ask<R>(),\n      RTE.flatMap(deps => {\n        // Check if circuit should reset\n        if (\n          state === 'open' &&\n          Date.now() - lastFailure > resetTimeoutMs\n        ) {\n          state = 'half-open'\n        }\n\n        if (state === 'open') {\n          return RTE.left({ _tag: 'CircuitOpen' as const })\n        }\n\n        return pipe(\n          operation,\n          RTE.tap(() => {\n            if (state === 'half-open') {\n              state = 'closed'\n              failures = 0\n            }\n            return RTE.right(undefined)\n          }),\n          RTE.tapError(error => {\n            if (isFailure(error)) {\n              failures++\n              lastFailure = Date.now()\n              if (failures >= failureThreshold) {\n                state = 'open'\n              }\n            }\n            return RTE.right(undefined)\n          })\n        )\n      })\n    )\n}\n```\n\n## Testing Strategies\n\n### Mocking Dependencies\n\n```typescript\n// src/services/__tests__/user.service.test.ts\nimport * as TE from 'fp-ts/TaskEither'\nimport * as E from 'fp-ts/Either'\nimport * as O from 'fp-ts/Option'\nimport { describe, it, expect, vi } from 'vitest'\nimport * as UserService from '../user.service'\n\n// Create mock dependencies\nconst createMockDeps = (overrides: Partial<UserDeps> = {}): UserDeps => ({\n  db: {\n    users: {\n      findUnique: vi.fn(() => Promise.resolve(null)),\n      create: vi.fn(data => Promise.resolve({ id: '1', ...data })),\n      update: vi.fn((where, data) => Promise.resolve({ id: where.id, ...data })),\n    },\n  },\n  hasher: {\n    hash: vi.fn(password => TE.right(`hashed_${password}`)),\n    verify: vi.fn(() => TE.right(true)),\n  },\n  mailer: {\n    send: vi.fn(() => TE.right(undefined)),\n  },\n  ...overrides,\n})\n\ndescribe('UserService', () => {\n  describe('create', () => {\n    it('should create a user with hashed password', async () => {\n      const deps = createMockDeps()\n      const input = {\n        email: 'test@example.com',\n        password: 'secret123',\n        name: 'Test User',\n      }\n\n      const result = await UserService.create(input)(deps)()\n\n      expect(E.isRight(result)).toBe(true)\n      if (E.isRight(result)) {\n        expect(result.right.email).toBe(input.email)\n      }\n      expect(deps.hasher.hash).toHaveBeenCalledWith('secret123')\n    })\n\n    it('should fail when email already exists', async () => {\n      const existingUser = { id: '1', email: 'test@example.com' }\n      const deps = createMockDeps({\n        db: {\n          users: {\n            findUnique: vi.fn(() => Promise.resolve(existingUser)),\n            create: vi.fn(),\n          },\n        },\n      })\n\n      const result = await UserService.create({\n        email: 'test@example.com',\n        password: 'secret',\n        name: 'Test',\n      })(deps)()\n\n      expect(E.isLeft(result)).toBe(true)\n      if (E.isLeft(result)) {\n        expect(result.left._tag).toBe('EmailExists')\n      }\n    })\n  })\n\n  describe('findById', () => {\n    it('should return user when found', async () => {\n      const user = { id: '1', email: 'test@example.com', name: 'Test' }\n      const deps = createMockDeps({\n        db: {\n          users: {\n            findUnique: vi.fn(() => Promise.resolve(user)),\n          },\n        },\n      })\n\n      const result = await UserService.findById('1')(deps)()\n\n      expect(E.isRight(result)).toBe(true)\n      if (E.isRight(result)) {\n        expect(result.right).toEqual(user)\n      }\n    })\n\n    it('should return NotFound when user does not exist', async () => {\n      const deps = createMockDeps()\n\n      const result = await UserService.findById('nonexistent')(deps)()\n\n      expect(E.isLeft(result)).toBe(true)\n      if (E.isLeft(result)) {\n        expect(result.left._tag).toBe('UserNotFound')\n        expect(result.left.id).toBe('nonexistent')\n      }\n    })\n  })\n})\n```\n\n### Integration Testing with Test Containers\n\n```typescript\n// src/__tests__/integration/user.integration.test.ts\nimport { PostgreSqlContainer } from '@testcontainers/postgresql'\nimport { PrismaClient } from '@prisma/client'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as E from 'fp-ts/Either'\nimport { pipe } from 'fp-ts/function'\nimport { describe, it, expect, beforeAll, afterAll } from 'vitest'\nimport { buildDeps, destroyDeps, AppDeps } from '../../deps'\nimport * as UserService from '../../services/user.service'\n\ndescribe('UserService Integration', () => {\n  let container: PostgreSqlContainer\n  let deps: AppDeps\n\n  beforeAll(async () => {\n    // Start PostgreSQL container\n    container = await new PostgreSqlContainer().start()\n\n    // Build real dependencies with test database\n    process.env.DATABASE_URL = container.getConnectionUri()\n\n    const depsResult = await buildDeps()()\n    if (E.isLeft(depsResult)) {\n      throw new Error(`Failed to build deps: ${depsResult.left}`)\n    }\n    deps = depsResult.right\n\n    // Run migrations\n    await deps.db.$executeRaw`CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"`\n    // ... run Prisma migrations\n  }, 60000)\n\n  afterAll(async () => {\n    await destroyDeps(deps)()\n    await container.stop()\n  })\n\n  it('should create and retrieve a user', async () => {\n    // Create user\n    const createResult = await UserService.create({\n      email: 'integration@test.com',\n      password: 'password123',\n      name: 'Integration Test',\n    })(deps)()\n\n    expect(E.isRight(createResult)).toBe(true)\n    if (E.isLeft(createResult)) return\n\n    const user = createResult.right\n\n    // Retrieve user\n    const findResult = await UserService.findById(user.id)(deps)()\n\n    expect(E.isRight(findResult)).toBe(true)\n    if (E.isRight(findResult)) {\n      expect(findResult.right.email).toBe('integration@test.com')\n    }\n  })\n})\n```\n\n### Property-Based Testing\n\n```typescript\n// src/__tests__/property/user.property.test.ts\nimport * as fc from 'fast-check'\nimport * as E from 'fp-ts/Either'\nimport { describe, it, expect } from 'vitest'\nimport { validateEmail, validatePassword } from '../../validation'\n\ndescribe('Validation Properties', () => {\n  it('valid emails should pass validation', () => {\n    fc.assert(\n      fc.property(fc.emailAddress(), email => {\n        const result = validateEmail(email)\n        return E.isRight(result)\n      })\n    )\n  })\n\n  it('passwords meeting requirements should pass', () => {\n    const validPassword = fc\n      .tuple(\n        fc.stringOf(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz'), {\n          minLength: 4,\n        }),\n        fc.stringOf(fc.constantFrom(...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), {\n          minLength: 1,\n        }),\n        fc.stringOf(fc.constantFrom(...'0123456789'), { minLength: 1 }),\n        fc.stringOf(fc.constantFrom(...'!@#$%^&*'), { minLength: 1 })\n      )\n      .map(parts => parts.join(''))\n\n    fc.assert(\n      fc.property(validPassword, password => {\n        const result = validatePassword(password)\n        return E.isRight(result)\n      })\n    )\n  })\n\n  it('empty strings should fail email validation', () => {\n    const result = validateEmail('')\n    expect(E.isLeft(result)).toBe(true)\n  })\n})\n```\n\n## Quick Reference\n\n### Common Imports\n\n```typescript\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport * as TE from 'fp-ts/TaskEither'\nimport * as E from 'fp-ts/Either'\nimport * as O from 'fp-ts/Option'\nimport * as A from 'fp-ts/Array'\nimport * as T from 'fp-ts/Task'\nimport { pipe, flow } from 'fp-ts/function'\n```\n\n### RTE Cheat Sheet\n\n| Operation | Description |\n|-----------|-------------|\n| `RTE.right(a)` | Lift value into success |\n| `RTE.left(e)` | Create error |\n| `RTE.ask<R>()` | Get dependencies |\n| `RTE.fromTaskEither(te)` | Lift TaskEither |\n| `RTE.fromEither(e)` | Lift Either |\n| `RTE.fromOption(onNone)(o)` | Lift Option |\n| `RTE.flatMap(f)` | Chain operations |\n| `RTE.map(f)` | Transform success |\n| `RTE.mapLeft(f)` | Transform error |\n| `RTE.tap(f)` | Side effect on success |\n| `RTE.tapError(f)` | Side effect on error |\n| `RTE.orElse(f)` | Recover from error |\n| `RTE.getOrElse(f)` | Extract with fallback |\n\n### Service Template\n\n```typescript\n// Template for a new service\nimport * as RTE from 'fp-ts/ReaderTaskEither'\nimport { pipe } from 'fp-ts/function'\n\ntype MyServiceDeps = {\n  db: DatabaseClient\n  // ... other dependencies\n}\n\ntype MyServiceError =\n  | { _tag: 'NotFound'; id: string }\n  | { _tag: 'ValidationFailed'; reason: string }\n\nexport const myOperation = (\n  input: Input\n): RTE.ReaderTaskEither<MyServiceDeps, MyServiceError, Output> =>\n  pipe(\n    RTE.ask<MyServiceDeps>(),\n    RTE.flatMap(deps =>\n      // Your implementation here\n      RTE.right(output)\n    )\n  )\n```\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["backend","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity-skills"],"capabilities":["skill","source-sickn33","skill-fp-backend","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-backend","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34793 github stars · SKILL.md body (33,413 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-24T00:50:57.755Z","embedding":null,"createdAt":"2026-04-18T21:37:23.730Z","updatedAt":"2026-04-24T00:50:57.755Z","lastSeenAt":"2026-04-24T00:50:57.755Z","tsv":"'/../deps':2615 '/../services/user.service':2620 '/../validation':2775 '/api':1632 '/api/users':1635 '/array':257,2898 '/deno':10 '/either':1300,1448,1809,2321,2594,2764,2882 '/function':139,264,528,757,1093,1307,1455,1679,2056,2601,2914,3002 '/option':851,1817,2329,2890 '/payment.service':433 '/product.service':428 '/readertaskeither':124,241,544,773,1082,1292,1440,1672,2041,2866,2995 '/task':2906 '/taskeither':132,249,536,765,843,1074,1284,2049,2313,2586,2874 '/user.service':423,2341 '/users':1420 '0':546,1255,2104,2222,2225,2280 '0123456789':2818 '1':601,828,2122,2129,2361,2446,2496,2514,2815,2820,2824 '10':581 '1d':593 '2':673,2124 '4':2810 '400':1386,1560,1938 '401':1394,1595,1617,1946,1954 '403':1950 '404':1376,1653,1934 '409':1942 '500':1401,1659,1961 '503':1958 '60000':2682 'a.traverse':472 'abcdefghijklmnopqrstuvwxyz':2808,2813 'account':1235,1253,1263 'account.balance':1254 'accountid':1228,1246,1261 'afteral':2607,2683 'alreadi':2002,2440 'amount':1189,1202,1207,1215,1230,1250 'app':1625 'app.get':1419,1634 'app.use':1628,1631 'appdep':704,712,727,777,1192,1414,1466,1474,1498,1694,2613,2629 'apperror':155,183,196,778,798,1367 'applic':700 'architectur':76 'arg':953,960,970,975,982,987 'ask':3070 'async':571,628,651,733,790,1137,1338,1476,1507,1540,1574,1744,2400,2442,2492,2537,2631,2684,2697 'attempt':2089,2120,2127 'auth':1566 'authent':2008 'autherror':1826,1857 'author':1584 'await':637,734,737,793,1141,1348,1482,1517,1545,1564,1598,1621,1779,2415,2462,2512,2543,2636,2651,2668,2685,2688,2702,2728 'backbon':95 'backend':3,11,25,36,54,68,75,98 'backoff':2060 'balanc':1248 'base':1818,2746 'basedelaym':2069,2130 'bearer':1586 'beforeal':2606,2630 'bodi':1544,1550,1965 'boolean':2074,2215 'boundari':3078 'break':1382,1390,1398 'breaker':2197 'build':31,47,516,2640,2661 'builddep':709,795,2611,2652 'buildinfrastructur':617,716 'buildservic':687,720 'c':1477,1495,1504,1508,1519,1528,1541,1575,1638,1643,1745 'c.get':1513,1579,1749,1758 'c.json':1530,1554,1590,1614,1648,1655 'c.req.header':1583 'c.req.json':1546 'c.req.method':1776 'c.req.param':1640 'c.req.path':1778 'c.res.status':1790 'c.set':1479,1561,1618,1763 'cach':2133,2140,2160,2166,2181,2190,2191,2193 'cache.get':2187 'cache.set':2172 'cachecli':2141 'cachekey':2144,2173,2188 'calcul':479 'calculatetot':485 'case':897,909,921,1373,1383,1391,1931,1935,1939,1943,1947,1951,1955,1977,1984,1995,2004,2010,2017,2023 'caus':168,199,801,875,937,1177,1872 'chain':2948 'cheat':2916 'check':302,2244,2756 'checkemailuniqu':305,388 'circuit':2196,2246 'circuitopen':2238,2265 'circuitst':2199,2218 'clarif':3072 'cleanup':722,742,813 'clear':3045 'close':2200,2219,2278 'common':2856 'complet':1786 'compos':415 'composit':65 'concept':84 'config':104,149,150,547,551,569,597,605,608,609,618,619,668 'config.database.url':636 'config.redis.url':653 'conflict':1905 'conflicterror':1825,1847,1849,1912,1914,1940,1996 'connect':639,1098 'console.error':822 'console.log':810 'const':177,288,322,348,355,387,411,442,565,616,629,686,708,724,774,788,791,880,942,1058,1110,1139,1183,1226,1260,1315,1342,1346,1364,1408,1471,1490,1511,1515,1535,1543,1547,1571,1577,1581,1596,1606,1624,1700,1725,1741,1747,1751,1877,1889,1904,1924,1967,2062,2088,2136,2206,2267,2345,2401,2404,2413,2443,2449,2460,2493,2501,2510,2538,2541,2649,2700,2721,2726,2789,2802,2832,2846,3020 'constructor':1875 'contain':519,2568,2625,2634,2635 'container.getconnectionuri':2648 'container.stop':2689 'context':1429,1459,1496,1505,1509,1661,1683,1699,1739 'contextdep':1693,1711 'contextvariablemap':1464 'convert':1308,1484 'core':83 'count':986,1048 'creat':285,289,500,969,1018,1738,2342,2356,2391,2394,2458,2671,2692,2698,2928 'createbcrypthash':692 'createcircuitbreak':2207 'createemailservic':697 'createinput':945,972,1020 'createjwtservic':694 'createlogg':663 'createmockdep':2346,2403,2451,2503,2540 'createord':443 'createorderrecord':507 'createrediscli':652 'createrepositori':943,1060 'createresult':2701,2714,2719 'createresult.right':2723 'createtransferrecord':1212 'createuserinput':291 'creditaccount':1205 'criteria':3081 'crypto.randomuuid':1755 'ctx':1695,1718,1752,1766 'ctx.requestid':1774,1788 'ctx.starttime':1723,1793 'data':383,971,978,1019,1026,1030,1038,1247,1359,1361,1529,1531,2358,2362,2366,2370 'databas':103,552,572,645,830,2645 'databasecli':146,268,440,3006 'databaseerror':167,198 'datasourc':633 'date.now':1722,1762,1792,2252,2291 'db':145,188,267,299,333,366,398,439,610,626,634,665,669,949,1114,1225,2350,2452,2504,3005 'db.users.count':379 'db.users.create':314 'db.users.findbyid':192 'db.users.findmany':372 'db.users.findunique':336,401 'dberror':858,885,890,995,1012,1022,1033,1043,1052,1127,1155 'debitaccount':1200,1227 'declar':1460 'decrement':1249 'default':1399,1959 'defin':140,151,222 'delay':2092,2118,2123 'deleg':951 'delegate.count':1055 'delegate.create':1025 'delegate.delete':1046 'delegate.findmany':1015 'delegate.findunique':999 'delegate.update':1036 'delet':981,1039 'deni':2015 'deno':53 'dep':144,182,726,783,786,804,807,818,1132,1143,1343,1351,1457,1465,1473,1480,1481,1512,1514,1520,1578,1580,1630,1748,1750,1764,1765,2087,2099,2243,2402,2418,2450,2470,2502,2515,2539,2546,2628,2662,2664,2687,2711,2731,3031 'depend':20,62,142,514,518,549,603,675,701,748,1467,2303,2344,2642,2932,3008 'dependencies/environment':102 'deps.db':735,1135,2669 'deps.hasher.hash':2432 'deps.jwt.verify':1600 'deps.logger.info':1770,1784 'deps.redis.quit':738 'depsresult':2650,2655 'depsresult.left':2663 'depsresult.right':2665 'describ':2331,2388,2390,2484,2603,2621,2766,2776,3049 'descript':2919 'destroydep':725,817,2612,2686 'detail':1558,1973,1990 'develop':12,99 'disconnect':736,1099 'domain':152,1159 'domainerror':1822,1927,1970 'e':90,106,195,200,594,599,642,647,654,659,739,744,797,802,1116,1122,1126,1154,1173,1295,1318,1329,1333,1443,1492,1499,1503,1804,2065,2073,2078,2082,2095,2142,2151,2155,2214,2232,2236,2316,2589,2759,2877,2927,2938 'e.fold':1354,1524 'e.isleft':1611,2472,2477,2548,2553,2654,2718,2850 'e.isright':2420,2425,2517,2522,2713,2733,2738,2794,2837 'effect':2961,2967 'either':107,112,2940 'elaps':1721,1791 'email':281,303,389,403,412,2406,2439,2447,2464,2497,2704,2781,2788,2792,2844 'emailexist':280,409,2483 'emailservic':272,685 'empti':2840 'end':1783 'environ':3061 'environment-specif':3060 'error':69,109,153,568,596,598,621,644,646,656,658,711,729,741,743,889,892,938,1153,1160,1163,1165,1169,1171,1178,1332,1355,1357,1362,1366,1378,1388,1396,1403,1406,1502,1525,1527,1555,1591,1615,1642,1649,1656,1658,1705,1733,1735,1794,1798,1819,1918,1926,1962,1969,1971,1980,1987,1998,2007,2013,2020,2026,2030,2072,2101,2106,2109,2184,2195,2213,2285,2288,2658,2929,2957,2969,2974 'error._tag':1372,1645,1930,1976 'error.code':896 'error.field':1992,2001 'error.message':1389,1994 'error.meta':903,915,927 'error.reason':1397 'error.required':2016 'error.resource':1379,1981,1999 'executeraw':2670 'exist':405,406,456,2003,2441,2536,2675 'existingus':2444,2457 'exit':815 'expect':2333,2419,2427,2431,2471,2479,2516,2524,2547,2555,2560,2605,2712,2732,2740,2768,2849 'expert':3066 'expir':592,2022 'expiresin':563,590 'exponenti':2059 'export':229,287,321,354,441,702,707,723,941,1109,1182,1314,1470,1489,1534,1570,1724,1740,1876,1888,1903,1923,1966,2061,2135,2205,3019 'express':1266,1276,1312 'extend':1113,2139 'extens':2672 'extract':2977 'f':2947,2951,2955,2959,2965,2971,2976 'factori':940 'fail':823,1557,1989,2437,2659,2843 'failur':2177,2221,2279,2289,2293 'failurethreshold':2208,2294 'fallback':2131,2979 'fast':2755 'fast-check':2754 'fc':2752,2804 'fc.assert':2785,2828 'fc.constantfrom':2807,2812,2817,2822 'fc.emailaddress':2787 'fc.property':2786,2829 'fc.stringof':2806,2811,2816,2821 'field':867,871,902,914,916,1840,1852,1891,1900,1908,1916,1991 'find':318,350 'findbyid':323,2485 'findmani':356,959,1003 'findresult':2727,2734,2739 'findresult.right.email':2741 'finduniqu':952,991,2352,2454,2506 'flow':2909 'foreignkeyviol':870,913 'found':216,1381,1652,1983,2491 'fp':2,15,23,40,57,122,130,137,239,247,255,262,526,534,542,755,763,771,841,849,1072,1080,1091,1282,1290,1298,1305,1438,1446,1453,1670,1677,1807,1815,2039,2047,2054,2311,2319,2327,2584,2592,2599,2762,2864,2872,2880,2888,2896,2904,2912,2993,3000 'fp-backend':1 'fp-ts':14,22,39,56,121,129,136,238,246,254,261,525,533,541,754,762,770,840,848,1071,1079,1090,1281,1289,1297,1304,1437,1445,1452,1669,1676,1806,1814,2038,2046,2053,2310,2318,2326,2583,2591,2598,2761,2863,2871,2879,2887,2895,2903,2911,2992,2999 'fromid':1185,1201 'full':699 'function':4,19,27,74,97,176,231,513 'get':466,2180,2931 'getdep':1320,1344 'getus':178 'getuserhandl':1409,1422 'half':2203,2257,2275 'half-open':2202,2256,2274 'handl':1064,1795 'handleerror':1365,1418 'handler':1310,1324,1349,1363,1488,1494,1518 'hash':2372,2376,2398 'hashedpassword':312,317 'hasher':269,300,680,691,2371 'hasher.hash':309 'hierarchi':1799 'hono':1423,1428,1432,1462,1487,1627 'http':1920 'id':160,179,193,211,277,320,324,338,349,863,932,1245,1421,1636,1641,1760,1834,1881,1887,2360,2368,2445,2495,3013 'implement':3033 'import':117,125,133,234,242,250,258,419,424,429,522,529,537,751,758,766,836,844,852,1067,1075,1083,1087,1270,1277,1285,1293,1301,1427,1433,1441,1449,1665,1673,1802,1810,2034,2042,2050,2306,2314,2322,2330,2337,2571,2575,2579,2587,2595,2602,2610,2616,2750,2757,2765,2771,2857,2859,2867,2875,2883,2891,2899,2907,2988,2996 'info':1703,1727,1729 'infra':688,718,719,721 'infra.config':698 'infra.config.jwt':695 'infrastructur':602,607,622,677,689,705 'infrastructureerror':1827,1867,1869,1956,2024 'inject':21,63,515,1468 'input':290,315,2405,2417,3022,3023,3075 'input.email':306,2430 'input.password':310 'insid':1219 'instanceof':893 'instead':1223 'insufficientfund':1258 'integr':2564,2623,2709 'integration@test.com':2705,2743 'interfac':1463 'intern':1404,1657 'invalid':1608 'invalidpassword':284 'involv':61 'isfailur':2212,2287 'isol':80 'item':446,471,474,487,510 'item.productid':476 'join':907 'json':1377,1387,1395,1402 'jwt':560,586,682,693 'jwtservic':683 'lastfailur':2224,2253,2290 'layer':220,545,600,672 'left':108,821,1147 'let':2216,2220,2223,2624,2627 'level':1702 'lift':2922,2935,2939,2944 'limit':3037 'loadconfig':566,714 'log':1697,1726,1767,1781 'logger':105,147,148,189,614,615,661,667,671,1717 'logger.info':215 'loggerlevel':1720 'logwithcontext':1701,1728,1731,1734 'mailer':271,684,696,2382 'main':789,829 'mani':351 'map':1922,2825 'match':3046 'maxattempt':2067,2128 'meet':2798 'messag':164,1706,1842,1893,1901,1993 'meta':1708 'method':1775 'middlewar':1264,1267,1313,1424,1469,1533,1567,1736 'middlewarehandl':1430,1475,1539,1573,1743 'migrat':2667,2681 'minlength':2809,2814,2819,2823 'miss':3083 'mock':2302,2343 'model':861,926,944,957,968,1013,1023,1034,1044 'modelnam':928 'modul':224,228,1461 'myoper':3021 'myservicedep':3004,3025 'myserviceerror':3010,3026 'name':917,2410,2468,2499,2708 'need':73 'never':1712 'new':595,631,643,655,740,1626,2113,2637,2657,2986 'next':1341,1478,1483,1542,1565,1576,1622,1746,1780 'nextfunct':1273 'node.js':9,51 'node.js/deno':8 'nonexist':2545,2563 'notfound':157,208,1374,1878,2531,3012 'notfounderror':1823,1829,1831,1883,1885,1932,1978 'null':958,1166,2355 'number':556,964,966,1008,1010,1053,1190,1231,1691,1928,2068,2070,2091,2093,2147,2209,2211 'o':846,1812,2324,2885,2943 'o.fromnullable':1002,1757 'o.option':996,1689 'object':1164,1709 'omit':1096 'onerror':1331,1356,1501,1526 'onnon':2942 'open':2201,2204,2251,2258,2261,2276,2296 'oper':831,879,882,888,2075,2098,2148,2163,2229,2270,2918,2949 'option':2945 'order':451,501 'orderdep':435,449 'ordererror':450 'orderitem':447 'ossp':2678 'output':3027,3036,3055 'overrid':2347,2387 'p2002':898 'p2003':910 'p2025':922 'pagin':353,1006,1017 'paginatedresult':362 'paginationparam':358 'param':357,386 'params.limit':376 'params.offset':374 'parseint':577 'part':2826 'partial':2348 'parts.join':2827 'pass':2783,2801 'password':316,2374,2377,2399,2408,2466,2706,2797,2831,2835 'password123':2707 'passwordhash':270,681 'path':1777 'pattern':6,26,29,77,221,1265,1662,1796 'payment':489,491,506,512 'paymentservic':431 'paymentservice.charge':495 'paymentservice.paymentdeps':438 'permiss':2014,3076 'pipe':134,185,190,259,296,301,330,334,363,368,395,399,452,459,470,494,523,623,713,731,752,780,784,794,805,997,1088,1129,1196,1236,1241,1302,1352,1450,1522,1599,1674,1714,2051,2084,2097,2111,2157,2162,2185,2240,2269,2596,2908,2997,3028 'pool':579 'poolsiz':555,576 'postgresql':2633 'postgresqlcontain':2572,2626,2638 'prisma':630,638,641,832,854,878,1061,2680 'prisma.prismaclientknownrequesterror':894 'prisma.user':1062 'prisma/client':856,1086,2578 'prismacli':611,632,853,950,1084,1097,1115,2576 'process':488 'process.env.database':574,2646 'process.env.db':578 'process.env.jwt':588,591 'process.env.redis':584 'process.exit':827 'product':467,469,483,486,504,509 'productservic':426 'productservice.findbyid':475 'productservice.productdeps':437 'program':5,28,746,775,806,1118,1142 'promis':883,956,967,973,980,985,990,1510,2114 'promise.resolve':2354,2359,2367,2456,2508 'properti':2745,2778 'property-bas':2744 'provid':1594 'quick':2854 'r':89,100,1112,1120,1125,1317,1323,1328,2064,2077,2081,2115,2117,2138,2150,2154,2227,2231,2235 'rather':78 're':1157 're-throw':1156 'reader':101 'readertaskeith':17,71,85,88 'real':2641 'reason':172,1607,3017 'recordnotfound':860,925 'recov':2972 'recoveri':2031 'redi':557,582,612,649,657,666,670 'rediscli':613 'refactor':49 'refer':2855 'remain':2090,2103,2121 'replac':1585 'repositori':939 'req':1321,1325,1339,1345,1350,1411,1415 'req.app.locals.deps':1412 'req.params.id':1417 'request':1271,1322,1326,1660,1681,1768,1771,1782,1785 'request-scop':1680 'requestcontext':1685,1696,1753 'requesthandl':1274,1337 'requestid':1686,1754,1773,1787 'requir':1862,2009,2799,3074 'requireauth':1572,1633 'res':1334,1340,1358,1368 'res.json':1360 'res.status':1375,1385,1393,1400 'reset':2248 'resettimeoutm':2210,2254 'resourc':158,209,1832,1850,1879,1886,1906,1915 'respons':1272,1335,1369,1506,1964 'rest':2161 'result':792,1140,1347,1353,1516,1523,1548,1597,1612,2168,2170,2174,2414,2421,2426,2461,2473,2478,2511,2518,2523,2542,2549,2554,2790,2795,2833,2838,2847,2851 'result._tag':820,1146 'result.data':1563 'result.error.flatten':1559 'result.left':826,1149,2480,2556 'result.left.id':2561 'result.left.reason':1616 'result.right':1152,1620,2525 'result.right.email':2428 'result.success':1552 'retri':2057 'retriev':2694,2724 'return':640,899,911,923,934,1151,1170,1174,1521,1553,1589,1613,1647,1654,1933,1937,1941,1945,1949,1953,1957,1960,1979,1986,1997,2006,2012,2019,2025,2107,2110,2125,2226,2262,2268,2281,2297,2488,2530,2720,2793,2836 'review':3067 'right':113 'rollback':1150 'rte':86,119,230,236,539,768,1077,1287,1309,1435,1485,1569,1667,2036,2861,2915,2990 'rte.applicativepar':473 'rte.ask':186,297,331,364,396,781,1130,1237,1715,2085,2158,2241,2930,3029 'rte.bind':457,468,481,490,1198,1203,1208 'rte.do':453,1197 'rte.flatmap':187,201,298,307,311,332,339,365,397,404,502,782,1131,1238,1716,2086,2159,2189,2242,2946,3030 'rte.fromeither':2937 'rte.fromio':214,1719 'rte.fromoption':2941 'rte.fromtaskeither':191,308,313,335,367,400,787,1133,1240,2126,2171,2186,2933 'rte.getorelse':2975 'rte.left':206,344,407,2194,2263,2926 'rte.map':1216,2950 'rte.mapleft':194,462,477,498,2954 'rte.orelse':2183,2970 'rte.readertaskeither':181,292,326,359,391,448,776,1119,1124,1191,1232,1327,1497,1710,2076,2080,2149,2153,2230,2234,3024 'rte.right':204,342,413,484,2192,2282,2298,2920,3035 'rte.tap':212,2169,2271,2958 'rte.taperror':2284,2964 'run':745,812,2666,2679 'safe':34 'safeti':3077 'schema':1537 'schema.safeparse':1549 'scope':1682,3048 'secret':561,587,589,2467 'secret123':2409,2434 'send':2383 'server':811,1405 'servic':37,64,175,219,223,226,416,674,679,690,706,1181,1870,2027,2980,2987 'settimeout':2116 'sheet':2917 'shouldretri':2071,2105 'side':2960,2966 'size':580 'skill':3040 'skill-fp-backend' 'skip':373,963,1007 'smart':1874 'snippet':82 'source-sickn33' 'specif':3062 'src/__tests__/integration/user.integration.test.ts':2570 'src/__tests__/property/user.property.test.ts':2749 'src/context.ts':1664 'src/deps.ts':521 'src/errors.ts':1801 'src/lib/db.ts':835 'src/lib/recovery.ts':2033 'src/lib/transaction.ts':1066 'src/main.ts':750 'src/middleware/fp-express.ts':1269 'src/middleware/fp-hono.ts':1426 'src/services/__tests__/user.service.test.ts':2305 'src/services/order.service.ts':418 'src/services/user.service.ts':233 'start':825,1769,1772,2632,2639 'startserv':785 'starttim':1690,1761 'startuperror':800 'state':2217,2250,2255,2260,2273,2277,2295 'status':1789,1921 'stop':3068 'store':1456 'strategi':2301 'string':159,161,165,173,180,278,282,325,390,445,554,559,562,564,862,864,868,872,906,919,930,1186,1188,1229,1687,1707,1833,1835,1841,1843,1851,1853,1855,1863,1871,1880,1882,1892,1894,1907,1909,1911,1972,2145,2841,3014,3018 'structur':225 'substitut':3058 'success':114,2165,2925,2953,2963,3080 'switch':895,1371,1929,1975 'tag':156,162,166,170,197,207,275,279,283,345,408,799,859,865,869,873,900,912,924,935,1167,1175,1257,1603,1830,1838,1848,1858,1860,1864,1868,1884,1898,1913,2237,2264,2481,2557,3011,3015 'take':375,965,1009 'target':904 'task':60,3044 'taskeith':2936 'te':127,244,531,760,838,1069,1279,2044,2308,2581,2869,2934 'te.bind':370,377,625,648,660 'te.do':369,624 'te.flatmap':715,803,1252,2119 'te.fromio':809 'te.fromtask':2112 'te.left':1256,2108 'te.map':380,664,717,1001 'te.mapleft':796,1602 'te.orelse':2100 'te.right':662,1262,2375,2380,2385 'te.tap':808 'te.taperror':816 'te.taskeither':567,620,710,728,884,994,1011,1021,1032,1042,1051,2094 'te.trycatch':570,627,650,732,887,1134,1242 'templat':2981,2983 'temporarili':2028 'test':2300,2411,2469,2500,2565,2567,2644,2710,2747,3064 'test@example.com':2407,2448,2465,2498 'testabl':35 'testcontainers/postgresql':2574 'throw':1148,1158,2656 'tobe':2422,2429,2474,2482,2519,2550,2558,2562,2715,2735,2742,2852 'todberror':1251 'toequal':2526 'tohandl':1316,1410 'tohavebeencalledwith':2433 'tohonohandl':1491,1637 'tohttpstatus':1925 'toid':1187,1206 'token':1582,1588,1593,1601,1609,2021 'tokenexpir':1865,1952,2018 'toordererror':463,478,499 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'toresponsebodi':1968 'total':378,382,385,480,482,493,497,505,511 'transact':1063,1101,1107,1136,1220 'transfer':1194,1209,1217,1218 'transfererror':1193,1234 'transferfund':1184 'transform':2952,2956 'treat':3053 'tri':2178 'true':2381,2423,2475,2520,2551,2716,2736,2853 'ts':16,24,41,58,123,131,138,240,248,256,263,527,535,543,756,764,772,842,850,1073,1081,1092,1283,1291,1299,1306,1439,1447,1454,1671,1678,1808,1816,2040,2048,2055,2312,2320,2328,2585,2593,2600,2763,2865,2873,2881,2889,2897,2905,2913,2994,3001 'ttlsecond':2146,2175 'tupl':2805 'tx':1105,1138,1144,1222,1239 'tx.account.update':1243 'txclient':1095,1106 'txdep':1104,1121,1233 'type':33,67,92,110,143,154,265,273,434,550,606,678,703,857,1094,1103,1684,1692,1797,1820,1821,1828,1836,1846,1856,1866,2198,3003,3009 'type-saf':32 'typeof':1162 'typescript':116,232,417,520,749,834,1065,1268,1425,1663,1800,2032,2304,2569,2748,2858,2982 'unauthent':1859,1944,2005 'unauthor':171,1392,1604,1861,1948,2011 'unavail':2029 'undefin':414,2283,2299,2386 'uniqu':304 'uniqueviol':866,901 'unknown':169,876,908,920,931,933,1845,1873,1896,1974 'unknowndberror':874,936,1176 'updat':974,1027,2363 'updateinput':946,979,1031 'url':553,558,573,575,583,585,635,2647 'usag':1057,1179,1407,1623 'use':13,38,44,1102,1221,1568,3038 'user':184,202,203,205,210,213,217,286,295,329,340,341,343,371,381,384,455,458,492,496,503,508,1619,1650,1759,2351,2396,2412,2453,2489,2494,2505,2509,2527,2533,2696,2699,2722,2725 'user.id':218,2730 'userdep':266,293,327,360,392,2349 'usererror':274,294,328,361,393 'userid':444,461,1688,1756 'usernotfound':276,346,1646,2559 'userrepo':1059 'userservic':421,2339,2389,2618,2622 'userservice.create':2416,2463,2703 'userservice.findbyid':460,1416,1639,2513,2544,2729 'userservice.userdeps':436 'util':81 'uuid':2677 'uuid-ossp':2676 'valid':454,464,1532,1536,1556,1890,1988,2777,2780,2784,2845,3063 'validatedbodi':1562 'validateemail':2772,2791,2848 'validatepassword':2773,2834 'validationerror':163,1384,1824,1837,1839,1897,1899,1936,1985 'validationfail':3016 'validpassword':2803,2830 'valu':115,1844,1854,1895,1902,1910,1917,2134,2182,2923 'verifi':2378 'vi':2334 'vi.fn':2353,2357,2364,2373,2379,2384,2455,2459,2507 'vitest':2336,2609,2770 'void':394,730,779,1336,1370,1713 'warn':1704,1730,1732 'where.id':2369 'wheremani':948,962,989,1005,1050 'whereuniqu':947,955,977,984,993,1029,1041 'withcontext':1742 'withdep':1472,1629 'withfallback':2137 'withretri':2063 'withtransact':1111,1195 'wrap':877 'wrapper':833,1108 'wrapprisma':881,998,1014,1024,1035,1045,1054 'z.zodschema':1538","prices":[{"id":"db4ab689-748d-46d6-a9de-5852bcf370d9","listingId":"9e79d68b-c9ab-49b9-872d-eb48b7807d37","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:37:23.730Z"}],"sources":[{"listingId":"9e79d68b-c9ab-49b9-872d-eb48b7807d37","source":"github","sourceId":"sickn33/antigravity-awesome-skills/fp-backend","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-backend","isPrimary":false,"firstSeenAt":"2026-04-18T21:37:23.730Z","lastSeenAt":"2026-04-24T00:50:57.755Z"}],"details":{"listingId":"9e79d68b-c9ab-49b9-872d-eb48b7807d37","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"fp-backend","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34793,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-24T00:28:59Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"a03345c2ae4e0e30db9342cfcb391d724cde13a5","skill_md_path":"skills/fp-backend/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/fp-backend"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"fp-backend","description":"Functional programming patterns for Node.js/Deno backend development using fp-ts, ReaderTaskEither, and functional dependency injection"},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/fp-backend"},"updatedAt":"2026-04-24T00:50:57.755Z"}}