whatiskadudoing/fp-ts-skills/fp-backend

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Functional programming patterns for Node.js/Deno backend development using fp-ts, ReaderTaskEither, and functional dependency injection

Install

npx vskill@latest install whatiskadudoing/fp-ts-skills/fp-backend

SKILL.md

fp-ts Backend Patterns

Functional programming patterns for building type-safe, testable backend services using fp-ts.

Core Concepts

ReaderTaskEither (RTE)

The ReaderTaskEither<R, E, A> type is the backbone of functional backend development:

  • R (Reader): Dependencies/environment (database, config, logger)
  • E (Either left): Error type
  • A (Either right): Success value
typescript
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
// Define your dependencies
type Deps = {
db: DatabaseClient
logger: Logger
config: Config
}
// Define domain errors
type AppError =
| { _tag: 'NotFound'; resource: string; id: string }
| { _tag: 'ValidationError'; message: string }
| { _tag: 'DatabaseError'; cause: unknown }
| { _tag: 'Unauthorized'; reason: string }
// A service function
const getUser = (id: string): RTE.ReaderTaskEither<Deps, AppError, User> =>
pipe(
RTE.ask<Deps>(),
RTE.flatMap(({ db, logger }) =>
pipe(
RTE.fromTaskEither(db.users.findById(id)),
RTE.mapLeft((e): AppError => ({ _tag: 'DatabaseError', cause: e })),
RTE.flatMap(user =>
user
? RTE.right(user)
: RTE.left({ _tag: 'NotFound', resource: 'User', id })
),
RTE.tap(user => RTE.fromIO(() => logger.info(`Found user: ${user.id}`)))
)
)
)

Service Layer Patterns

Defining Service Modules

Structure services as modules exporting RTE functions:

typescript
// src/services/user.service.ts
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'
type UserDeps = {
db: DatabaseClient
hasher: PasswordHasher
mailer: EmailService
}
type UserError =
| { _tag: 'UserNotFound'; id: string }
| { _tag: 'EmailExists'; email: string }
| { _tag: 'InvalidPassword' }
// Create user
export const create = (
input: CreateUserInput
): RTE.ReaderTaskEither<UserDeps, UserError, User> =>
pipe(
RTE.ask<UserDeps>(),
RTE.flatMap(({ db, hasher }) =>
pipe(
// Check email uniqueness
checkEmailUnique(input.email),
RTE.flatMap(() =>
RTE.fromTaskEither(hasher.hash(input.password))
),
RTE.flatMap(hashedPassword =>
RTE.fromTaskEither(
db.users.create({
...input,
password: hashedPassword,
})
)
)
)
)
)
// Find by ID
export const findById = (
id: string
): RTE.ReaderTaskEither<UserDeps, UserError, User> =>
pipe(
RTE.ask<UserDeps>(),
RTE.flatMap(({ db }) =>
pipe(
RTE.fromTaskEither(db.users.findUnique({ where: { id } })),
RTE.flatMap(user =>
user
? RTE.right(user)
: RTE.left({ _tag: 'UserNotFound' as const, id })
)
)
)
)
// Find many with pagination
export const findMany = (
params: PaginationParams
): RTE.ReaderTaskEither<UserDeps, UserError, PaginatedResult<User>> =>
pipe(
RTE.ask<UserDeps>(),
RTE.flatMap(({ db }) =>
RTE.fromTaskEither(
pipe(
TE.Do,
TE.bind('users', () => db.users.findMany({
skip: params.offset,
take: params.limit,
})),
TE.bind('total', () => db.users.count()),
TE.map(({ users, total }) => ({
data: users,
total,
...params,
}))
)
)
)
)
const checkEmailUnique = (
email: string
): RTE.ReaderTaskEither<UserDeps, UserError, void> =>
pipe(
RTE.ask<UserDeps>(),
RTE.flatMap(({ db }) =>
pipe(
RTE.fromTaskEither(db.users.findUnique({ where: { email } })),
RTE.flatMap(existing =>
existing
? RTE.left({ _tag: 'EmailExists' as const, email })
: RTE.right(undefined)
)
)
)
)
Composing Services
typescript
// src/services/order.service.ts
import * as UserService from './user.service'
import * as ProductService from './product.service'
import * as PaymentService from './payment.service'
type OrderDeps = UserService.UserDeps &
ProductService.ProductDeps &
PaymentService.PaymentDeps & {
db: DatabaseClient
}
export const createOrder = (
userId: string,
items: OrderItem[]
): RTE.ReaderTaskEither<OrderDeps, OrderError, Order> =>
pipe(
RTE.Do,
// Validate user exists
RTE.bind('user', () =>
pipe(
UserService.findById(userId),
RTE.mapLeft(toOrderError)
)
),
// Validate and get products
RTE.bind('products', () =>
pipe(
items,
A.traverse(RTE.ApplicativePar)(item =>
ProductService.findById(item.productId)
),
RTE.mapLeft(toOrderError)
)
),
// Calculate total
RTE.bind('total', ({ products }) =>
RTE.right(calculateTotal(products, items))
),
// Process payment
RTE.bind('payment', ({ user, total }) =>
pipe(
PaymentService.charge(user, total),
RTE.mapLeft(toOrderError)
)
),
// Create order
RTE.flatMap(({ user, products, total, payment }) =>
createOrderRecord(user, products, items, total, payment)
)
)

Functional Dependency Injection

Building the Dependency Container
typescript
// src/deps.ts
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as RTE from 'fp-ts/ReaderTaskEither'
// Layer 0: Config (no dependencies)
type Config = {
database: { url: string; poolSize: number }
redis: { url: string }
jwt: { secret: string; expiresIn: string }
}
const loadConfig = (): TE.TaskEither<Error, Config> =>
TE.tryCatch(
async () => ({
database: {
url: process.env.DATABASE_URL!,
poolSize: parseInt(process.env.DB_POOL_SIZE || '10'),
},
redis: { url: process.env.REDIS_URL! },
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: process.env.JWT_EXPIRES || '1d',
},
}),
(e) => new Error(`Config error: ${e}`)
)
// Layer 1: Infrastructure (depends on config)
type Infrastructure = {
config: Config
db: PrismaClient
redis: RedisClient
logger: Logger
}
const buildInfrastructure = (
config: Config
): TE.TaskEither<Error, Infrastructure> =>
pipe(
TE.Do,
TE.bind('db', () =>
TE.tryCatch(
async () => {
const prisma = new PrismaClient({
datasources: { db: { url: config.database.url } },
})
await prisma.$connect()
return prisma
},
(e) => new Error(`Database error: ${e}`)
)
),
TE.bind('redis', () =>
TE.tryCatch(
async () => createRedisClient(config.redis.url),
(e) => new Error(`Redis error: ${e}`)
)
),
TE.bind('logger', () => TE.right(createLogger())),
TE.map(({ db, redis, logger }) => ({
config,
db,
redis,
logger,
}))
)
// Layer 2: Services (depends on infrastructure)
type Services = {
hasher: PasswordHasher
jwt: JwtService
mailer: EmailService
}
const buildServices = (infra: Infrastructure): Services => ({
hasher: createBcryptHasher(),
jwt: createJwtService(infra.config.jwt),
mailer: createEmailService(infra.config),
})
// Full application dependencies
export type AppDeps = Infrastructure & Services
export const buildDeps = (): TE.TaskEither<Error, AppDeps> =>
pipe(
loadConfig(),
TE.flatMap(buildInfrastructure),
TE.map(infra => ({
...infra,
...buildServices(infra),
}))
)
// Cleanup
export const destroyDeps = (deps: AppDeps): TE.TaskEither<Error, void> =>
pipe(
TE.tryCatch(
async () => {
await deps.db.$disconnect()
await deps.redis.quit()
},
(e) => new Error(`Cleanup error: ${e}`)
)
)
Running Programs with Dependencies
typescript
// src/main.ts
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as RTE from 'fp-ts/ReaderTaskEither'
const program: RTE.ReaderTaskEither<AppDeps, AppError, void> = pipe(
RTE.ask<AppDeps>(),
RTE.flatMap(deps =>
pipe(
startServer(deps),
RTE.fromTaskEither
)
)
)
const main = async () => {
const result = await pipe(
buildDeps(),
TE.mapLeft((e): AppError => ({ _tag: 'StartupError', cause: e })),
TE.flatMap(deps =>
pipe(
program(deps),
TE.tap(() => TE.fromIO(() => console.log('Server running'))),
// Cleanup on exit
TE.tapError(() => destroyDeps(deps))
)
)
)()
if (result._tag === 'Left') {
console.error('Failed to start:', result.left)
process.exit(1)
}
}
main()

Database Operations

Prisma Wrappers
typescript
// src/lib/db.ts
import * as TE from 'fp-ts/TaskEither'
import * as O from 'fp-ts/Option'
import { PrismaClient, Prisma } from '@prisma/client'
type DbError =
| { _tag: 'RecordNotFound'; model: string; id: string }
| { _tag: 'UniqueViolation'; field: string }
| { _tag: 'ForeignKeyViolation'; field: string }
| { _tag: 'UnknownDbError'; cause: unknown }
// Wrap Prisma operations
const wrapPrisma = <A>(
operation: () => Promise<A>
): TE.TaskEither<DbError, A> =>
TE.tryCatch(operation, (error): DbError => {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.code) {
case 'P2002':
return {
_tag: 'UniqueViolation',
field: (error.meta?.target as string[])?.join(', ') || 'unknown',
}
case 'P2003':
return {
_tag: 'ForeignKeyViolation',
field: error.meta?.field_name as string || 'unknown',
}
case 'P2025':
return {
_tag: 'RecordNotFound',
model: error.meta?.modelName as string || 'unknown',
id: 'unknown',
}
}
}
return { _tag: 'UnknownDbError', cause: error }
})
// Repository factory
export const createRepository = <
Model,
CreateInput,
UpdateInput,
WhereUnique,
WhereMany
>(
db: PrismaClient,
delegate: {
findUnique: (args: { where: WhereUnique }) => Promise<Model | null>
findMany: (args: { where?: WhereMany; skip?: number; take?: number }) => Promise<Model[]>
create: (args: { data: CreateInput }) => Promise<Model>
update: (args: { where: WhereUnique; data: UpdateInput }) => Promise<Model>
delete: (args: { where: WhereUnique }) => Promise<Model>
count: (args?: { where?: WhereMany }) => Promise<number>
}
) => ({
findUnique: (where: WhereUnique): TE.TaskEither<DbError, O.Option<Model>> =>
pipe(
wrapPrisma(() => delegate.findUnique({ where })),
TE.map(O.fromNullable)
),
findMany: (
where?: WhereMany,
pagination?: { skip: number; take: number }
): TE.TaskEither<DbError, Model[]> =>
wrapPrisma(() => delegate.findMany({ where, ...pagination })),
create: (data: CreateInput): TE.TaskEither<DbError, Model> =>
wrapPrisma(() => delegate.create({ data })),
update: (
where: WhereUnique,
data: UpdateInput
): TE.TaskEither<DbError, Model> =>
wrapPrisma(() => delegate.update({ where, data })),
delete: (where: WhereUnique): TE.TaskEither<DbError, Model> =>
wrapPrisma(() => delegate.delete({ where })),
count: (where?: WhereMany): TE.TaskEither<DbError, number> =>
wrapPrisma(() => delegate.count({ where })),
})
// Usage
const userRepo = createRepository(prisma, prisma.user)
Transaction Handling
typescript
// src/lib/transaction.ts
import * as TE from 'fp-ts/TaskEither'
import * as RTE from 'fp-ts/ReaderTaskEither'
import { PrismaClient } from '@prisma/client'
import { pipe } from 'fp-ts/function'
type TxClient = Omit<
PrismaClient,
'$connect' | '$disconnect' | '$on' | '$transaction' | '$use'
>
type TxDeps = { tx: TxClient }

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
11
Repository forks
3
7-day trend score
12

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/whatiskadudoing/fp-ts-skills/fp-backend/badge)](https://verified-skill.com/skills/whatiskadudoing/fp-ts-skills/fp-backend)

Something wrong with this skill or its page? Tell us.