Skip to content

Basic Usage

Master the basic operations of Pipeline.

Creating Pipeline

Basic Creation

typescript
import { createPipeline } from 'farrow-pipeline'

// Specify input/output types
const pipeline = createPipeline<InputType, OutputType>()

// Example: string to number
const parser = createPipeline<string, number>()

// Example: user validation
interface User {
  name: string
  age: number
}

const userValidator = createPipeline<unknown, User | null>()

Type Inference

typescript
// TypeScript will infer types based on middleware
const pipeline = createPipeline<number, string>()
  .use((x, next) => next(x + 1))     // x: number
  .use((x) => `Result: ${x}`)        // x: number, returns string

Adding Middleware

Method 1: Single Addition

typescript
const pipeline = createPipeline<number, string>()

pipeline.use((input, next) => {
  console.log('Middleware 1')
  return next(input)
})

pipeline.use((input, next) => {
  console.log('Middleware 2')
  return next(input)
})

pipeline.use((input) => {
  console.log('Last one')
  return `Result: ${input}`
})

Method 2: Batch Addition

typescript
const middleware1 = (input, next) => next(input + 1)
const middleware2 = (input, next) => next(input * 2)
const middleware3 = (input) => `Value: ${input}`

pipeline.use(middleware1, middleware2, middleware3)

Method 3: Chain Calls

typescript
const pipeline = createPipeline<number, string>()
  .use((x, next) => {
    console.log('Step 1:', x)
    return next(x + 1)
  })
  .use((x, next) => {
    console.log('Step 2:', x)
    return next(x * 2)
  })
  .use((x) => {
    console.log('Step 3:', x)
    return `Completed: ${x}`
  })

Method 4: Extract Middleware Functions

typescript
// Define reusable middleware
function validateInput(input: number, next) {
  if (input < 0) {
    throw new Error('Input must be positive')
  }
  return next(input)
}

function doubleValue(input: number, next) {
  return next(input * 2)
}

function formatResult(input: number) {
  return `Result: ${input}`
}

// Combine and use
const pipeline = createPipeline<number, string>()
  .use(validateInput)
  .use(doubleValue)
  .use(formatResult)

Running Pipeline

Basic Run

typescript
const pipeline = createPipeline<number, string>()
  .use((x, next) => next(x * 2))
  .use((x) => `Value: ${x}`)

const result = pipeline.run(5)
console.log(result)  // "Value: 10"

Run with Options

typescript
const pipeline = createPipeline<number, string>()
  .use((x, next) => next(x * 2))
  // Note: no final middleware

// Provide onLast as default handler
const result = pipeline.run(5, {
  onLast: (input) => `Default: ${input}`
})

console.log(result)  // "Default: 10"

Purpose of onLast

typescript
// Scenario 1: All middleware call next
const pipeline1 = createPipeline<number, string>()
  .use((x, next) => next(x + 1))
  .use((x, next) => next(x * 2))
  // No termination middleware

pipeline1.run(5, {
  onLast: (x) => `Final value: ${x}`  // Will be called
})  // "Final value: 12"

// Scenario 2: Has termination middleware
const pipeline2 = createPipeline<number, string>()
  .use((x, next) => next(x + 1))
  .use((x) => `Early end: ${x}`)  // Termination middleware

pipeline2.run(5, {
  onLast: (x) => `Final value: ${x}`  // Will not be called
})  // "Early end: 6"

Middleware Order

Execution Order Matters

typescript
// Order A
const pipelineA = createPipeline<number, number>()
  .use((x, next) => next(x + 10))  // Add first
  .use((x, next) => next(x * 2))   // Multiply second

console.log(pipelineA.run(5, { onLast: x => x }))  // (5 + 10) * 2 = 30

// Order B
const pipelineB = createPipeline<number, number>()
  .use((x, next) => next(x * 2))   // Multiply first
  .use((x, next) => next(x + 10))  // Add second

console.log(pipelineB.run(5, { onLast: x => x }))  // (5 * 2) + 10 = 20

Pre and Post Processing

typescript
const pipeline = createPipeline<number, number>()

// Middleware 1: Pre/post processing
pipeline.use((input, next) => {
  console.log('Pre 1:', input)      // 5
  const result = next(input + 1)     // Pass 6
  console.log('Post 1:', result)    // 14
  return result + 1                  // Return 15
})

// Middleware 2: Pre/post processing
pipeline.use((input, next) => {
  console.log('Pre 2:', input)      // 6
  const result = next(input * 2)     // Pass 12
  console.log('Post 2:', result)    // 12
  return result + 2                  // Return 14
})

// Middleware 3: Termination
pipeline.use((input) => {
  console.log('Terminate:', input)   // 12
  return input                       // Return 12
})

pipeline.run(5)
// Output:
// Pre 1: 5
// Pre 2: 6
// Terminate: 12
// Post 2: 12
// Post 1: 14
// Final result: 15

Conditional Execution

Decide whether to continue based on input

typescript
const pipeline = createPipeline<number, string>()
  .use((input, next) => {
    if (input < 0) {
      return 'Error: Negative number'  // Don't call next, return early
    }
    if (input === 0) {
      return 'Zero'
    }
    return next(input)  // Only continue for positive numbers
  })
  .use((input, next) => {
    if (input > 100) {
      return 'Error: Too large'
    }
    return next(input)
  })
  .use((input) => {
    return `Normal value: ${input}`
  })

console.log(pipeline.run(-5))   // "Error: Negative number"
console.log(pipeline.run(0))    // "Zero"
console.log(pipeline.run(50))   // "Normal value: 50"
console.log(pipeline.run(200))  // "Error: Too large"

Multi-path Branching

typescript
type Action =
  | { type: 'add', value: number }
  | { type: 'multiply', value: number }
  | { type: 'reset' }

const calculator = createPipeline<{ state: number, action: Action }, number>()
  .use(({ state, action }, next) => {
    switch (action.type) {
      case 'add':
        return next({ state: state + action.value, action })
      case 'multiply':
        return next({ state: state * action.value, action })
      case 'reset':
        return 0  // Return directly, don't continue
      default:
        return next({ state, action })
    }
  })
  .use(({ state }) => state)

console.log(calculator.run({ state: 10, action: { type: 'add', value: 5 } }))       // 15
console.log(calculator.run({ state: 10, action: { type: 'multiply', value: 3 } }))  // 30
console.log(calculator.run({ state: 10, action: { type: 'reset' } }))               // 0

Error Handling

try-catch Wrapper

typescript
const safePipeline = createPipeline<number, string>()

// Global error handling middleware
safePipeline.use((input, next) => {
  try {
    return next(input)
  } catch (error) {
    console.error('Pipeline error:', error)
    return 'Execution failed'
  }
})

// Middleware that might throw exception
safePipeline.use((input, next) => {
  if (input < 0) {
    throw new Error('Negative numbers not allowed')
  }
  return next(input * 2)
})

safePipeline.use((input) => {
  return `Success: ${input}`
})

console.log(safePipeline.run(5))   // "Success: 10"
console.log(safePipeline.run(-5))  // "Execution failed" (catches exception)

Error Passing

typescript
type Result<T> = { ok: true, value: T } | { ok: false, error: string }

const pipeline = createPipeline<number, Result<number>>()
  .use((input, next) => {
    if (input < 0) {
      return { ok: false, error: 'Negative number' }
    }
    return next(input)
  })
  .use((input, next) => {
    if (input > 100) {
      return { ok: false, error: 'Too large' }
    }
    return next(input)
  })
  .use((input) => {
    return { ok: true, value: input * 2 }
  })

const result = pipeline.run(50)
if (result.ok) {
  console.log('Result:', result.value)
} else {
  console.log('Error:', result.error)
}

Practical Examples

Example 1: Data Transformation Pipeline

typescript
interface RawUser {
  name: string
  age: string
  email: string
}

interface User {
  name: string
  age: number
  email: string
  isAdult: boolean
}

const userTransformer = createPipeline<RawUser, User>()
  .use((input, next) => {
    // Convert age
    return next({
      ...input,
      age: parseInt(input.age, 10)
    } as any)
  })
  .use((input: any) => {
    // Add isAdult field
    return {
      name: input.name,
      age: input.age,
      email: input.email,
      isAdult: input.age >= 18
    }
  })

const raw = { name: 'Alice', age: '25', email: 'alice@example.com' }
const user = userTransformer.run(raw)
console.log(user)  // { name: 'Alice', age: 25, email: 'alice@example.com', isAdult: true }

Example 2: Request Processing Pipeline

typescript
interface Request {
  url: string
  method: string
  body?: any
}

interface Response {
  status: number
  body: any
}

const requestPipeline = createPipeline<Request, Response>()
  // Logging
  .use((req, next) => {
    console.log(`${req.method} ${req.url}`)
    return next(req)
  })
  // Authentication
  .use((req, next) => {
    if (!req.headers?.authorization) {
      return { status: 401, body: { error: 'Unauthorized' } }
    }
    return next(req)
  })
  // Routing
  .use((req) => {
    if (req.url === '/users') {
      return { status: 200, body: { users: [] } }
    }
    return { status: 404, body: { error: 'Not found' } }
  })

This is a third-party Farrow documentation site | Built with ❤️ and TypeScript