| name | fp-backend |
| description | Functional programming patterns for Node.js/Deno backend development using fp-ts, ReaderTaskEither, and functional dependency injection |
| risk | unknown |
| source | community |
| version | 1.0.0 |
| author | kadu |
| tags | ["fp-ts","typescript","backend","functional-programming","node","deno","dependency-injection","reader-task-either"] |
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
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
type Deps = {
db: DatabaseClient
logger: Logger
config: Config
}
type AppError =
| { _tag: 'NotFound'; resource: string; id: string }
| { _tag: 'ValidationError'; message: string }
| { _tag: 'DatabaseError'; cause: unknown }
| { _tag: 'Unauthorized'; reason: string }
const getUser = (id: string): RTE.ReaderTaskEither<Deps, AppError, User> =>
pipe(
.<>(),
.(
(
.(db..(id)),
.((e): ({ : , : e })),
.(
user
? .(user)
: .({ : , : , id })
),
.( .( logger.()))
)
)
)
Service Layer Patterns
Defining Service Modules
Structure services as modules exporting RTE functions:
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' }
export const create = (
input: CreateUserInput
): RTE.ReaderTaskEither<UserDeps, UserError, User> =>
pipe(
RTE.ask<UserDeps>(),
.(
(
(input.),
.(
.(hasher.(input.))
),
.(
.(
db..({
...input,
: hashedPassword,
})
)
)
)
)
)
findById = (
:
): .<, , > =>
(
.<>(),
.(
(
.(db..({ : { id } })),
.(
user
? .(user)
: .({ : , id })
)
)
)
)
findMany = (
:
): .<, , <>> =>
(
.<>(),
.(
.(
(
.,
.(, db..({
: params.,
: params.,
})),
.(, db..()),
.( ({
: users,
total,
...params,
}))
)
)
)
)
checkEmailUnique = (
:
): .<, , > =>
(
.<>(),
.(
(
.(db..({ : { email } })),
.(
existing
? .({ : , email })
: .()
)
)
)
)
Composing Services
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,
RTE.bind('user', () =>
pipe(
UserService.findById(userId),
RTE.mapLeft(toOrderError)
)
),
.(,
(
items,
A.(.)(
.(item.)
),
.(toOrderError)
)
),
.(,
.((products, items))
),
.(,
(
.(user, total),
.(toOrderError)
)
),
.(
(user, products, items, total, payment)
)
)
Functional Dependency Injection
Building the Dependency Container
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as RTE from 'fp-ts/ReaderTaskEither'
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: {
: process..!,
: process.. || ,
},
}),
()
)
= {
:
:
:
:
}
buildInfrastructure = (
:
): .<, > =>
(
.,
.(,
.(
() => {
prisma = ({
: { : { : config.. } },
})
prisma.$connect()
prisma
},
()
)
),
.(,
.(
() => (config..),
()
)
),
.(, .(())),
.( ({
config,
db,
redis,
logger,
}))
)
= {
:
:
:
}
buildServices = (: ): ({
: (),
: (infra..),
: (infra.),
})
= &
buildDeps = (): .<, > =>
(
(),
.(buildInfrastructure),
.( ({
...infra,
...(infra),
}))
)
destroyDeps = (: ): .<, > =>
(
.(
() => {
deps..$disconnect()
deps..()
},
()
)
)
Running Programs with Dependencies
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 =>
(
(deps),
.( .( .())),
.( (deps))
)
)
)()
(result. === ) {
.(, result.)
process.()
}
}
()
Database Operations
Prisma Wrappers
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 }
const wrapPrisma = <A>(
operation: () => Promise<A>
): TE.TaskEither<DbError, A> =>
TE.tryCatch(operation, (error): DbError => {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
switch (error.) {
:
{
: ,
: (error.?. [])?.() || ,
}
:
{
: ,
: error.?. || ,
}
:
{
: ,
: error.?. || ,
: ,
}
}
}
{ : , : error }
})
createRepository = <
,
,
,
,
> ({
: (: ): .<, O.<>> =>
(
( delegate.({ where })),
.(O.)
),
: (
?: ,
?: { : ; : }
): .<, []> =>
( delegate.({ where, ...pagination })),
: (: ): .<, > =>
( delegate.({ data })),
: (
: ,
:
): .<, > =>
( delegate.({ where, data })),
: (: ): .<, > =>
( delegate.({ where })),
: (?: ): .<, > =>
( delegate.({ where })),
})
userRepo = (prisma, prisma.)
Transaction Handling
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 }
export const withTransaction = <R extends { db: PrismaClient }, E, A>(
program: RTE.ReaderTaskEither<R & TxDeps, E, A>
): RTE.ReaderTaskEither<R, E | DbError, A> =>
pipe(
RTE.ask<R>(),
RTE.flatMap(deps =>
.(
.(
deps..$transaction( tx => {
result = ({ ...deps, tx })()
(result. === ) {
result.
}
result.
}),
(error): E | {
( error === && error !== && error) {
error E
}
{ : , : error }
}
)
)
)
)
transferFunds = (
: ,
: ,
:
): .<, , > =>
(
(
.,
.(, (fromId, amount)),
.(, (toId, amount)),
.(,
(, to, amount)
),
.( transfer)
)
)
debitAccount = (
: ,
:
): .<, , > =>
(
.<>(),
.(
.(
(
.(
tx..({
: { : accountId },
: { : { : amount } },
}),
toDbError
),
.(
account. <
? .({ : , accountId })
: .(account)
)
)
)
)
)
Middleware Patterns
Express Middleware
import { Request, Response, NextFunction, RequestHandler } from 'express'
import * as TE from 'fp-ts/TaskEither'
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
export const toHandler =
<R, E, A>(
getDeps: (req: Request) => R,
handler: (req: Request) => RTE.ReaderTaskEither<R, E, A>,
onError: (error: E, res: Response) => void
): RequestHandler =>
async (req, res, next) => {
const deps = getDeps(req)
const result = (req)(deps)()
(
result,
E.(
(error, res),
res.(data)
)
)
}
handleError = (: , : ): {
(error.) {
:
res.().({ : error. + })
:
res.().({ : error. })
:
res.().({ : error. })
:
res.().({ : })
}
}
getUserHandler = (
req... ,
.(req..),
handleError
)
app.(, getUserHandler)
Hono Middleware
import { Hono, Context, MiddlewareHandler } from 'hono'
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
declare module 'hono' {
interface ContextVariableMap {
deps: AppDeps
}
}
export const withDeps = (deps: AppDeps): MiddlewareHandler =>
async (c, next) => {
c.set('deps', deps)
await next()
}
export const toHonoHandler =
<E, A>(
handler: (c: Context) => RTE.ReaderTaskEither<AppDeps, E, A>,
onError: (error: E, c: Context) => Response
) =>
async (: ): <> => {
deps = c.()
result = (c)(deps)()
(
result,
E.(
(error, c),
c.(data)
)
)
}
validate =
<T>(: z.<T>):
(c, next) => {
body = c..()
result = schema.(body)
(!result.) {
c.(
{ : , : result..() },
)
}
c.(, result.)
()
}
: = (c, next) => {
deps = c.()
token = c..()?.(, )
(!token) {
c.({ : }, )
}
result = (
deps..(token),
.( ({ : , : }))
)()
(E.(result)) {
c.({ : result.. }, )
}
c.(, result.)
()
}
app = ()
app.(, (deps))
app.(, requireAuth)
app.(
,
(
.(c..()),
{
(error. === ) {
c.({ : }, )
}
c.({ : }, )
}
)
)
Request Context Pattern
import * as RTE from 'fp-ts/ReaderTaskEither'
import { pipe } from 'fp-ts/function'
type RequestContext = {
requestId: string
userId: O.Option<string>
startTime: number
}
type ContextDeps = AppDeps & { ctx: RequestContext }
const logWithContext =
(level: 'info' | 'warn' | 'error') =>
(message: string, meta?: object): RTE.ReaderTaskEither<ContextDeps, never, void> =>
pipe(
RTE.ask<ContextDeps>(),
RTE.flatMap(({ logger, ctx }) =>
RTE.fromIO(() =>
loggerlevel,
: .() - ctx.,
})
)
)
)
log = {
: (),
: (),
: (),
}
: = (c, next) => {
deps = c.()
: = {
: crypto.(),
: O.(c.()?.),
: .(),
}
c.(, { ...deps, ctx })
deps..(, {
: ctx.,
: c..,
: c..,
})
()
deps..(, {
: ctx.,
: c..,
: .() - ctx.,
})
}
Error Handling Patterns
Typed Error Hierarchy
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
type DomainError =
| NotFoundError
| ValidationError
| ConflictError
| AuthError
| InfrastructureError
type NotFoundError = {
_tag: 'NotFoundError'
resource: string
id: string
}
type ValidationError = {
_tag: 'ValidationError'
field: string
message: string
value?: unknown
}
type ConflictError = {
_tag: 'ConflictError'
resource: string
field: string
value: string
}
type AuthError =
| { _tag: 'Unauthenticated' }
| { _tag: 'Unauthorized'; required: string }
| { : }
= {
:
:
:
}
notFound = (: , : ): ({
: ,
resource,
id,
})
validation = (
: ,
: ,
?:
): ({
: ,
field,
message,
value,
})
conflict = (
: ,
: ,
:
): ({
: ,
resource,
field,
value,
})
toHttpStatus = (: ): {
(error.) {
:
:
:
:
:
:
:
:
}
}
toResponseBody = (
:
): { : ; ?: } => {
(error.) {
:
{ : }
:
{
: ,
: { : error., : error. },
}
:
{
: ,
}
:
{ : }
:
{ : }
:
{ : }
:
{ : }
}
}
Error Recovery
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import { pipe } from 'fp-ts/function'
export const withRetry =
<R, E, A>(
maxAttempts: number,
baseDelayMs: number,
shouldRetry: (error: E) => boolean
) =>
(
operation: RTE.ReaderTaskEither<R, E, A>
): RTE.ReaderTaskEither<R, E, A> =>
pipe(
RTE.ask<R>(),
RTE.flatMap(deps => {
const attempt = (
remaining: number,
delay: number
): TE.TaskEither<E, A> =>
pipe(
operation(deps),
TE.orElse(error => {
(remaining <= || !(error)) {
.(error)
}
(
.( ( (r, delay))),
.( (remaining - , delay * ))
)
})
)
.((maxAttempts - , baseDelayMs))
})
)
withFallback =
<R { : }, E, A>
(
: .<R, E, A>
): .<R, E, A> =>
(
.<R>(),
.(
(
operation,
.(
.(cache.(cacheKey, result, ttlSeconds))
),
.(
(
.(cache.<A>(cacheKey)),
.(
cached ? .(cached) : .(error)
)
)
)
)
)
)
= | |
createCircuitBreaker = <E> {
: =
failures =
lastFailure =
<R, A>(
: .<R, E, A>
): .<R, E | { : }, A> =>
(
.<R>(),
.( {
(
state === &&
.() - lastFailure > resetTimeoutMs
) {
state =
}
(state === ) {
.({ : })
}
(
operation,
.( {
(state === ) {
state =
failures =
}
.()
}),
.( {
((error)) {
failures++
lastFailure = .()
(failures >= failureThreshold) {
state =
}
}
.()
})
)
})
)
}
Testing Strategies
Mocking Dependencies
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
import { describe, it, expect, vi } from 'vitest'
import * as UserService from '../user.service'
const createMockDeps = (overrides: Partial<UserDeps> = {}): UserDeps => ({
db: {
users: {
findUnique: vi.fn(() => Promise.resolve(null)),
create: vi.fn(data => Promise.resolve({ id: '1', ...data })),
update: vi.fn((where, data) => Promise.resolve({ id: where.id, ...data })),
},
},
: {
: vi.( .()),
: vi.( .()),
},
: {
: vi.( .()),
},
...overrides,
})
(, {
(, {
(, () => {
deps = ()
input = {
: ,
: ,
: ,
}
result = .(input)(deps)()
(E.(result)).()
(E.(result)) {
(result..).(input.)
}
(deps..).()
})
(, () => {
existingUser = { : , : }
deps = ({
: {
: {
: vi.( .(existingUser)),
: vi.(),
},
},
})
result = .({
: ,
: ,
: ,
})(deps)()
(E.(result)).()
(E.(result)) {
(result..).()
}
})
})
(, {
(, () => {
user = { : , : , : }
deps = ({
: {
: {
: vi.( .(user)),
},
},
})
result = .()(deps)()
(E.(result)).()
(E.(result)) {
(result.).(user)
}
})
(, () => {
deps = ()
result = .()(deps)()
(E.(result)).()
(E.(result)) {
(result..).()
(result..).()
}
})
})
})
Integration Testing with Test Containers
import { PostgreSqlContainer } from '@testcontainers/postgresql'
import { PrismaClient } from '@prisma/client'
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { buildDeps, destroyDeps, AppDeps } from '../../deps'
import * as UserService from '../../services/user.service'
describe('UserService Integration', () => {
let container: PostgreSqlContainer
let deps: AppDeps
beforeAll(async () => {
container = await new PostgreSqlContainer().start()
process.. = container.()
depsResult = ()()
(E.(depsResult)) {
()
}
deps = depsResult.
deps..
}, )
( () => {
(deps)()
container.()
})
(, () => {
createResult = .({
: ,
: ,
: ,
})(deps)()
(E.(createResult)).()
(E.(createResult))
user = createResult.
findResult = .(user.)(deps)()
(E.(findResult)).()
(E.(findResult)) {
(findResult..).()
}
})
})
Property-Based Testing
import * as fc from 'fast-check'
import * as E from 'fp-ts/Either'
import { describe, it, expect } from 'vitest'
import { validateEmail, validatePassword } from '../../validation'
describe('Validation Properties', () => {
it('valid emails should pass validation', () => {
fc.assert(
fc.property(fc.emailAddress(), email => {
const result = validateEmail(email)
return E.isRight(result)
})
)
})
it('passwords meeting requirements should pass', () => {
const validPassword = fc
.tuple(
fc.stringOf(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz'), {
minLength: 4,
}),
fc.stringOf(fc.constantFrom(...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), {
minLength: 1,
}),
fc.stringOf(fc.(...), { : }),
fc.(fc.(...), { : })
)
.( parts.())
fc.(
fc.(validPassword, {
result = (password)
E.(result)
})
)
})
(, {
result = ()
(E.(result)).()
})
})
Quick Reference
Common Imports
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'
import * as A from 'fp-ts/Array'
import * as T from 'fp-ts/Task'
import { pipe, flow } from 'fp-ts/function'
RTE Cheat Sheet
| Operation | Description |
|---|
RTE.right(a) | Lift value into success |
RTE.left(e) | Create error |
RTE.ask<R>() | Get dependencies |
RTE.fromTaskEither(te) | Lift TaskEither |
RTE.fromEither(e) | Lift Either |
RTE.fromOption(onNone)(o) | Lift Option |
RTE.flatMap(f) | Chain operations |
RTE.map(f) | Transform success |
RTE.mapLeft(f) | Transform error |
RTE.tap(f) | Side effect on success |
RTE.tapError(f) | Side effect on error |
RTE.orElse(f) | Recover from error |
RTE.getOrElse(f) | Extract with fallback |
Service Template
import * as RTE from 'fp-ts/ReaderTaskEither'
import { pipe } from 'fp-ts/function'
type MyServiceDeps = {
db: DatabaseClient
}
type MyServiceError =
| { _tag: 'NotFound'; id: string }
| { _tag: 'ValidationFailed'; reason: string }
export const myOperation = (
input: Input
): RTE.ReaderTaskEither<MyServiceDeps, MyServiceError, Output> =>
pipe(
RTE.ask<MyServiceDeps>(),
RTE.flatMap(deps =>
RTE.right(output)
)
)