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

pleasant

v3.3.4

Published

Next-generation ES6 HTTP framework. Asynchronous and lightweight.

Downloads

11

Readme

NPM version version

Next-generation ES6 HTTP framework. Asynchronous and lightweight.

Getting started

Install it

$ npm install pleasant --save

Add start script in package.json

{
  "scripts": {
    "start": "pleasant index.js"
  }
}

Populate index.js

export default async server => {
  server.route({
    method: 'GET',
    url: '/hello'
    handler: async (req, res) => {
      res.send('Hello World')
    }
  })
}

Start the server

cli

... Is that it? Yes

Highlights

  • Getting started is easy!
  • Out-of-the-box support for ES modules
  • Asynchronous (Use async and await)
  • Middleware support (Connect/Express)
  • Flexible router (Express route pattern definitions)
  • Built-in request validation.
  • Built-in error handling.
  • Really fast (Benchmarks)
  • Native HTTP server

ES Modules

With the help of esm, pleasant has full out-of-the-box support for ES modules. You don't need to use babel/transpiling or the --experimental-modules flag.

So instead of:

// index.js
const something = require('something')
module.exports = async server => {}

You can do:

// index.js
import something from 'something'
export default async server => {}

Main file

The main file is where all the magic happens. You can specify it (server.js) by doing the following:

package.json

{
  "scripts": {
    "start": "pleasant server.js"
  }
}

... or CLI

$ pleasant server.js

The main file is registered using the default exported function, passing the server instance as argument.

// server.js
import cors from 'cors'

export default async server => {
  // Enable cors middleware
  server.use(cors())

  // Register a route
  server.route({
    method: 'GET',
    url: '/ping',
    handler: async (req, res) => {
      res.send({ message: 'pong' })
    }
  })
}

Plugins

Plugins are registered using the default exported function, passing the server instance as argument. See API for more details.

// index.js
export default async server => {
  // Register plugin
  await server.register(
    import('./routes/route-a')
  )
}
// routes/route-a.js
export default async server => {
  // Register route
  server.route({
    method: 'GET',
    url: '/',
    handler: async (req, res) => {
      // Send response
      res.send({ status: 'ok' })
    }
  })
}

Routing

pleasant is fully compatible with Express route pattern definitions.

// URL: /users/34/books/8989
server.route({
  method: 'GET',
  url: '/users/:userId/books/:bookId',
  handler: async (req, res) => {
    console.log(req.params)
    // { "userId": "34", "bookId": "8989" }
  }
})

Let’s say we wanted a :userId parameter in a route rule to match only a 6 digit integer.

The following regex parameter rule does that:

server.route({
  method: 'GET',
  url: '^/users/:userId([0-9]{6})',
  handler: async (req, res) => {
    const { userId } = req.params
    res.send(`Route match for user ID: ${userId}`);
  }
})

Middleware

You can enable connect/express middleware using server.use.

import cors from 'cors'

server.use(cors())

The following middleware is already included:

  • JSON body parser
  • Query parser

Route-specific middleware can be enabled like so:

import cors from 'cors'

const hello = (name) => (req, res, next) => {
  console.log(`Hello ${name}!`)
  next()
}

server.route({
  method: 'GET',
  url: '/',
  handler: [
    // Middleware #1
    cors(),

    // Middleware #2
    hello('World'),
    
    // Route handler
    async (req, res) => {
      res.send('Hello World')
    }
  ]
})

Validation

Validating data can be very helpful in making sure that your application is stable and secure. pleasant supports the incredible validator joi, which allows you to create your validations with a simple and clear object syntax.

Install:

npm install joi --save

Example:

import joi from 'joi'

server.route({
  method: 'GET',
  url: '/',
  validate: {
    query: {
      offset: joi.number().default(0),
      limit: joi.number().default(10).max(100)
    }
  },
  handler: async (req, res) => {
    const { offset, limit } = req.query
    res.send('A smile of joi!')
  }
})

URL: /?offset=0&limit=200

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "\"limit\" must be less than or equal to 100",
  "source": "query.limit"
}

Error handling

pleasant comes with a built-in error handler, which takes care of any errors that might occur.

If an error is thrown and not caught by you, the response will automatically be 500:

{
  "statusCode": 500,
  "error": "Internal Server Error",
  "message": "An internal server error occurred"
}

If the Error object that's thrown contains a statusCode property, that's used as the HTTP code to be sent.

You can define custom error-handling middleware last, after other server.use() and server.route() calls; for example:

server.use((err, req, res, next) => {
  // Handle error
  next()
})

Error responses

pleasant supports boom error objects.

Install:

npm install boom --save

Route example:

import boom from 'boom'

server.route({
  method: 'GET',
  url: '/',
  handler: async (req, res) => {
    // res.send(boom.badImplementation())
    // res.send(boom.badRequest())
    res.send(boom.notFound())
  }
})

Middleware example:

import boom from 'boom'

server.use((req, res, next) => {
  next(boom.forbidden())
})

Response example:

{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Not Found"
}

API

const server = pleasant()

Initialize pleasant. The pleasant() function is a top-level function exported by the pleasant module.

await server.register([prefix], plugin, [options = {}])

pleasant allows you to extend its functionalities with plugins. A plugin can be a set of routes, a server decorator or whatever.

  • prefix An optional path prefix used by any calls to server.route() and server.use().
  • plugin A dynamic or static module import. (Also accepts an array of plugins).
  • options An optional options object that's passed to the plugin(s)

Plugins are registered sequentially, each one running once the previous plugin has finished registering.

Examples:

// Register a single plugin
await server.register(
  // Plugin
  import('./awesome-plugin'),

  // Options
  {
    foo: true,
    bar: false
  }
)
// awesome-plugin.js
export default async (server, options) => {
  console.log(options) // { foo: true, bar: false }
}

Example with multiple (prefixed) plugins:

// Static plugin import
import staticPluginImport from './route-b'

// Register multiple plugins
await server.register(
  // Prefix
  "/api/v1",

  // Array of plugins
  [
    import("./routes/route-a"),
    staticPluginImport
  ],
  
  // Options
  {
    foo: true,
    bar: false
  }
)

server.use([path], ...middleware)

Use the given middleware function for all http methods on the given path, defaulting to the root path.

  • path The path for which the middleware function is invoked
  • middleware A middleware function
server.use((req, res, next) => {
  console.log('Logged')
  next()
})

Define error-handling middleware functions in the same way as other middleware functions, except error-handling functions have four arguments instead of three: (err, req, res, next). For example:

server.use((err, req, res, next) => {
  // Handle error
  next()
})

server.route(config)

Add a route

  • config
    • method HTTP method. Typically one of 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'. Also supports an array.
    • url The url for which the handler/middleware is invoked.
    • validate Request input validation rules for various request components. Uses joi for validation.
      • headers Validation rules for incoming request headers.
      • query Validation rules for an incoming request URI query component. The query is parsed into its individual key-value pairs and stored in req.query prior to validation.
      • body Validation rules for an incoming request body.
      • params Validation rules for incoming request path parameters
    • handler The function called to generate the response. can be one of either:
      • A middleware function
      • An array of middleware functions

Example:

import joi from 'joi'
import boom from 'boom'

server.route({
  method: 'GET',
  url: '/users',
  validate: {
    query: {
      offset: joi.number().default(0),
      limit: joi.number().default(10).max(100)
    }
  },
  handler: async (req, res) => {
    // res.send('Hello World')
    // res.send(200, { status: 'ok' })
    // res.send(boom.notFound())
    res.send({ status: 'ok' })
  }
})

The request and response objects are plain HTTP except for req.params, req.query, and res.send

req.params
This property is an object containing properties mapped to the named route “parameters”. For example, if you have the route /user/:name, then the “name” property is available as req.params.name. This object defaults to {}

req.query
This property is an object containing a property for each query string parameter in the route.

res.send([statusCode = 200], data = null)
Sends the HTTP response.

  • statusCode HTTP status code. Defaults to 200.
  • data If data is supplied it is sent in the response. Different input types are processed appropriately, and Content-Type and Content-Length are automatically set
    • Stream: data is piped as an octet-stream.
    • Buffer: data is written as an octet-stream.
    • object: data is serialized as JSON.
    • string: data is written as-is.
    • Error: boom is written as boom payload.

server.routes()

Returns an array of registered routes.

server.on(type, handler)

Register an event handler for the given type.

  • type Type of event to listen for, or "*" for all events
  • handler Function to call in response to given event

Example:

const logger = e => console.log('foo', e)
server.on('foo', logger)

pleasant only has one built-in event. This event is 'ready', which is emitted right before the server starts listening.

server.off(type, handler)

Remove an event handler for the given type.

  • type Type of event to unregister handler from, or "*"
  • handler Handler function to remove

Example:

const logger = e => console.log('foo', e)
server.off('foo', logger)

await server.emit(type, event)

Invoke all handlers for the given type. If present, "*" handlers are invoked after type-matched handlers.

  • type The event type to invoke
  • event Any value (object is recommended and powerful), passed to each handler

Example:

await server.emit('foo', { a: 'b' })

server.set(key, value)

Set the value of the key

Example:

server.set('foo', { a: 'b', c: 'd' })

server.get([key])

Get the value of the key

key is optional. Will return entire store if omitted.

If the key is not found, it will return undefined.

Example:

server.get('foo') // { a: 'b', c: 'd' }
server.get() // { foo: { a: 'b', c: 'd' } }

server.listen()

The node HTTP server listen() function.

server.close()

The node HTTP server close() function.

server.listener

The node HTTP server object.

Programmatic use

You can use pleasant programmatically by requiring directly:

const server = require('pleasant')()

server.route({
  method: 'GET',
  url: '/',
  handler: async (req, res) => {
    res.send('Hello World')
  }
})

server.listen(3000, (err) => {
  if (err) throw err
  console.log(`Accepting connections...`)
})

Benchmarks

This is a synthetic, "Hello World" benchmark that aims to evaluate the framework overhead.

Results are taken after 1 warm-up run. The tool used for results is the following:

wrk -t8 -c100 -d30s http://localhost:3000/
  • pleasant: 35,359 Req/Sec
  • http.createServer: 33,419 Req/Sec
  • Express: 15,466 Req/Sec
  • Hapi: 6,207 Req/Sec

How come pleasant is faster than http.createServer?

It's really not. But pleasant does event loop scheduling, that sometimes provides a performance boost. When multiple calls to res.send() are made, the functions are queued for execution. The entire queue is processed every event loop iteration.

CLI

$ pleasant -h

  Usage: pleasant [options] [entry]
  
  Options:
    -p, --port <n>  Port to listen on (defaults to 3000)
    -H, --host      The host on which server will run
    -v, --version   Output the version number
    -h, --help      Show this usage information

Inspiration