1.0.3 • Published 3 years ago

express-pipes v1.0.3

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

Express Pipes

A lightweight, zero dependency library that allows you to create modular, reusable components in express.

Inspired heavily by React and Koa

Installation

npm install express-pipes
yarn add express-pipes

Pipes

A pipe is simply an array of middleware functions that get executed in order.

const MyPipe = [
  (props) => {
    console.log('First!')
  },
  (props) => {
    console.log('Second!')
  },
  (props) => {
    console.log('Third!')
  },
]

Each function in the array accepts a props object containing req and res.

const Logger = [
  ({ req, res }) => {
    console.log(req.method, req.url)
  },
]

Pipes are composable You can nest pipes within themselves which allows you to breakdown and reuse logic across different components

const CleanUp1 = [
  (props) => {
    console.log('doing some clean up')
  },
]

const CleanUp2 = [
  (props) => {
    console.log('doing some more clean up')
  },
]

const CleanUp = [CleanUp1, CleanUp2]

const MessyPipe = [
  (props) => {
    console.log('making a mess')
  },
  CleanUp, // Runs CleanUp1 and CleanUp2
]

You can also create higher order components similar to React.

We could have used clean up as such:

function withCleanUp(component) {
  return [component, CleanUp1, CleanUp2]
}

const MessyPipe = withCleanUp([
  (props) => {
    console.log('making a mess')
  },
])

Passing Data

Instead of attaching data to the request object as is typical in express, you can attach to props. This avoids overcrowding the request object.

const AddUser = [
  (props) => {
    props.user = {
      username: 'foo',
    }
  },
  ({ user }) => {
    console.log('added user', user.username) // 'added user foo'
  },
]

Return Values

Returning a value from a function will call the respective res function.

const MyPipe = [
  (props) => {
    return { foo: 'bar' } // res.json({ foo: 'bar' })
  },
]

These are the corresponding functions that get called on return values

// objects use res.json()
(props) => {
  return { foo: 'bar' }
},

// strings use res.send()
(props) => {
  return 'hello'
},

// numbers use res.send()
(props) => {
  return 1
},

// null uses res.end()
(props) => {
  return null
},

// instances of Error are passed to express via next(error)
(props) => {
  return new Error('Woops!')
},

Throwing from a function will also get passed to express for error handling

(props) => {
  throw { message: 'Woops!' } // calls next({ message: 'Woops!' })
},

Registration

To register a component in a way that express can understand you need to use the pipe method on your component.

The pipe method transforms your component into express middleware

const { pipe } = require('express-pipes')

Then somewhere in your app

const Logger = [
  (props) => {
    console.log(props.req.method, props.req.url)
  },
]

app.use(pipe(Logger))

You may also wish to use the pipe method directly on components

const Logger = pipe([
  (props) => {
    console.log(props.req.method, props.req.url)
  },
])

app.use(Logger)

pipe is smart enough to know which components it has already transformed. You can use it on every component at no cost

const Component1 = pipe([
  () => {
    console.log('hello from component 1')
  },
])

const Component2 = pipe([
  Component1, // perfectly valid,
  () => console.log('hello from component 2')
])

const withComponent1And2(component) {
  return pipe([  // also valid
    Component1,
    Component2,
    component
  ])
}

Provider

The only other export apart from pipe (and type definitions) is provider

const { provider } = require('express-pipes')

Provider allows you to populate the props object with values to be consumed by pipes downstream

For instance you can use it to provide services to your pipes

// Define your services
const ServiceProvider = provider({
  validate: new ValidationService(),
  models: new ModelService(),
})

// Register them
app.use(ServiceProvider)

// Consume them
const Register = pipe([
  async (props) => {
    const { validate, models } = props
    const { email, name, password } = validate.register(req.body)
    const user = await models.User.create({ email, name, password })
    props.user = user
  },
  SendRegistrationEmail,
])

app.post('/register', Register)

Typescript

Strong type support is included as express pipes was built with typescript.

To define a pipe component use the Pipe type

import { Pipe } from 'express-pipes'

const Component: Pipe = [
  (props) => {
    // these will have types
    props.req
    props.res
  },
]

Custom Types

You will most likely be modifying the props object at some point in your application either with services or with data. In this case you will want to extends the props type.

Assuming we have an object somewhere in our code that exports our services

// ./services.ts

export default {
  jwt: new JwtService(),
  logger: new LoggerService(),
}

To Access these types across our project we will want to make use of Props and Pipe types both exported from the library.

import { Props, Pipe } from 'express-pipes'
import services from './services'

Now we can make use of these types to get type inferrence with the services.

Using default props type

const Component = [
  (props: Props<typeof services>) => {
    //
  },
]

Using custom props type

export type MyProps = Props<typeof services>

const Component = [
  (props: MyProps) => {
    //
  },
]

Using default pipe type

const Component: Pipe<MyProps> = [
  (props) => {
    //
  },
]

Using custom pipe type

type MyPipe = Pipe<MyProps>

const Component: MyPipe = [
  (props) => {
    //
  },
]

You should declare both a custom props type and a custom handler this will make it easy to get type inferrences accross all your components that use the custom props.

Usage with pipe requires the props type

const Component = pipe<MyProps>([
  (props) => {
    //
  },
])
1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago