npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

lindyhop

v0.1.8

Published

Express router with doc generation, validation and improved middleware support

Downloads

3

Readme

lindyhop

Express router with doc generation, validation and improved middleware support. See an example:

var express = require('express')
var lindyhop = require('lindyhop')

var app = express()
var lindy = lindyhop.hop(app)

var users = lindy.router('/users')

users.get('/:userId/foo', 'This is what this endpoint does')
  .middleware('auth', { rol: 'admin' })
  .middleware('pagination')
  .params((validate) => {
    validate
      .string('type', 'The type of foo you want to get')
      .optional()
      .trim()
      .lowerCase()
    validate
      .number('bar')
      .min(0)
      .max(100)
    validate
      .object('userId', 'The id of the user for something')
      .model(Users)
      .as('user')
      .in('path')
  })
  .run((params) => {
    // your business logic that returns data or a promise
  })

Installation

npm install lindyhop --save

Configuring an express app

var express = require('express')
var lindyhop = require('lindyhop')

var app = express()
var lindy = lindyhop.hop(app)

Routers

A router is a group of endpoints. You just need to specify the path in which these endpoints will be mounted. For example

var users = lindy.router('/users')

Routes

You can create routes from an existing router using the .get(), .post(), .put(), .delete() and .patch() methods. All these methods require two arguments. First the path of the route and then the description of the route for documentation purposes.

Routes have a .params() method where you validate the request parameters and a .run() method where you put your business logic and which receives the validated parameters.

users.get('/foo', 'This is what this endpoint does')
  .params((validate) => {
    validate
      .string('type', 'The type of foo you want to get')
      .optional()
      .trim()
      .lowerCase()
    validate
      .number('bar')
      .min(0)
      .max(100)
    validate
      .entity('userId', 'The id of the user for something')
      .model(Users)
      .as('user')
  })
  .run((params) => {
    // your business logic that returns data or a promise
  })

The returned data from the .run() method is automatically serialized to JSON.

Validations

Validations are described in the .params() method. An object is passed to the callback function of this method that allows you to describe validations. The structure is validate.[type](fieldName, description).specificMethod().

There are two built-in validators: string and number with the following methods:

String validations

  • .notEmpty() will return an error if the value is an empty string
  • .trim() will trim the value
  • .lowerCase() will lowercase the value
  • .upperCase() will uppercase the value

Number validations

  • .min(minValue) will return an error if the value passed is lower than that minimum value
  • .max(maxValue) will return an error if the value passed is higher than that maximum value

Additionally any validator has these methods:

  • .as() allows you to rename the parameter
  • .optional() makes the parameter optional, so no error will be returned if the parameter is not in the request
  • .array() will accept a single value or multiple values for this parameter while returning always an array to you

By default any GET or DELETE request gets the parameters from the query string, and in any other case it gets the values from the body. If you want to override this behavior you can specify a different source with .in() for each parameter.

  • .in('query') will look for the parameter in the query string
  • .in('header') will look for the parameter in a request header
  • .in('path') will look for the parameter in the URL (such as /users/:userId)
  • .in('formData') will look for the parameter in a request body

Custom validators

You can also create your custom validators. For example, let's see how to create a custom validation that reads objects by id using an ORM.

class EntityValidator extends lindyhop.AbstractValidator {
  // custom method
  model (clazz) {
    this.clazz = clazz
    return this
  }

  // mandatory method
  validate (value) {
    return this.clazz.findById(value)
      .then((obj) => {
        if (!obj && !this.isOptional) {
          return this.validationError(`User with id '${value}' not found`)
        }
        return obj
      })
  }
}

// register the validator
lindyhop.validator('entity', EntityValidator)

Then you can use it like this:

events.post('/invite', 'This is what this endpoint does')
  .params((validate) => {
    validate
      .entity('userId', 'The user to be invited')
      .model(Users) // calling our custom method
      .as('user')
    validate
      .entity('event', 'The event to which the user is invited')
      .model(Events) // calling our custom method
      .as('event')
  })
  .run((params) => {
    // params.user will contain the object from the database
  })

All validations are executed in parallel, so in our case the user and the event are read from the database in parallel.

Middleware

You can add your own middleware like this:

lindyhop.middleware('pagination', (req, res, options, params) => {
  params.offset = +req.query.offset || 0
  params.limit = 100
})

You need to specify the name of the middleware and then a function with the following parameters:

  • req: the request
  • res: the response
  • options: we will see this later
  • params: the params object that will be passed to the run() method of the controller

Then you can use it this way:

users.get('/foo', 'This is what this endpoint does')
  .middleware('pagination')
  .run((params) => {
    // here you have params.limit and params.offset
  })

You can optionally pass an options object to middleware() like this: .middleware('pagination', someOptions). That object will be passed to the middleware function as third parameter.

HTML output

By default JSON is always returned, but you can also render HTML this way:

// app is your express app
app.set('view engine', 'pug')
app.set('views', path.join(__dirname, 'views'))

users.get('/html', 'This is what this endpoint does')
  .outputs('html', 'index')
  .run((params) => {
    return { message: 'Hello world' }
  })

This endpoint will render the index.pug template passing { message: 'Hello world' } as render options. So your template could contain:

h1= message

Redirects

If you need to do an HTTP redirect you can do it with lindyhop.redirect(url, [permanent])

router.get('/regular', 'A regular redirect')
  .run((params) => {
    if (something) return lindyhop.redirect('http://example.com') // regular redirect (302)
    // else ...
  })

router.get('/permanent', 'A permanent redirect')
  .run((params) => {
    if (something) return lindyhop.redirect('http://example.com', true) // permanent redirect (301)
    // else ...
  })

Custom output serializers

You can create custom serializers easily:

lindyhop.output('pdf', 'application/pdf', (req, res, data, options) => {
  if (options.err) {
    // in case of error
  } else {
    // Use the response object (`res`) to return whatever you need.
    // In `data` you have the data returned by the controller.
    // `options` is the second (optional) argument passed to `.outputs('pdf', options)`
  }
})

Error responses

You can create meaningful errors with the rejects utility. For example:

var rejects = lindyhop.rejects

users.get('/foo', 'This is what this endpoint does')
  .run((params) => {
    if (something) {
      return rejects.forbidden('The reason of the bad request')
    }
  })

The available error types are:

  • rejects.internalError(): will return a 500 status code
  • rejects.forbidden(): will return a 403 status code
  • rejects.badRequest(): will return a 400 status code
  • rejects.notFound(): will return a 404 status code
  • rejects.unauthorized(): will return a 401 status code

All these methods can be used with one or two arguments.

With one string, such as rejects.internalError('Your message') it will return

{
  "error": "InternalError",
  "message": "Your messsage"
}

Passing two strings, such as rejects.internalError('DatabaseError', 'Your message') will return a response like this:

{
  "error": "DatabaseError",
  "message": "Your messsage"
}

If the controller throws an Error, it will return a response like this:

{
  "error": "InternalError",
  "message": "Error message"
}

And the error will be printed to the stderr with console.error().

If the controller returns a regular promise rejected, created with a string like Promise.reject('Your message') it will return a response like this:

{
  "error": "InternalError",
  "message": "Your messsage"
}

But if the rejection reason is not a string, like Promise.reject({ fail: 'reason' }), the rejection object will be serialized directly:

{
  "fail": "reason"
}

Validation errors contain an array of errors, each one containing a field with the name of the field that filed validation and a message describing the reason:

{
  "error": "ValidationError",
  "message": "Bad request. Check the errors",
  "errors": [
    {
      "field": "bar",
      "message": "'bar' must be a number. Received 'not-a-number'"
    },
    {
      "field": "userId",
      "message": "User with id '123' not found"
    }
  ]
}

Documentation

Calling lindy.docs() you will get a valid Swagger API description document. You can save the result in a file and use swagger-codegen to generate documentation or even generate an SDK for your API. Here's for example how you can generate static docs for your API:

swagger-codegen generate -i api.json -l html -o docs/

When configuring your express app you can pass an optional info object for swagger. Example

var lindy = lindyhop.hop(app, {
  'title': 'Swagger Sample App',
  'description': 'This is a sample server Petstore server.',
  'termsOfService': 'http://swagger.io/terms/',
  'contact': {
    'name': 'API Support',
    'url': 'http://www.swagger.io/support',
    'email': '[email protected]'
  },
  'license': {
    'name': 'Apache 2.0',
    'url': 'http://www.apache.org/licenses/LICENSE-2.0.html'
  },
  'version': '1.0.1'
})