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 🙏

© 2026 – Pkg Stats / Ryan Hefner

extrovert

v0.2.1

Published

The framework for framework-free JavaScript

Readme

extrovert

The framework for framework-free JavaScript

Write your application as plain functions, then expose those functions through Express or Netlify. Extrovert keeps routing, request mapping, and response handling at the edge so your application code remains ordinary JavaScript.

const actions = {
  users: {
    getUser: ({ db }) => async({ id }) => db.users.findById(id)
  }
};

There are no framework base classes, decorators, or special request objects in this code. The action receives its dependencies when the application starts and a plain parameters object when it runs. You can call it from an HTTP route, a test, a script, or another action.

Extrovert is the small bridge between those functions and the outside world:

  • applySpec() creates a nested service object from factory functions.
  • objectRouter() turns a nested object of functions into an Express router.
  • toRoute() adapts a function to an Express request handler.
  • pureMiddleware() adapts promise-based request middleware to Express.
  • toNetlifyFunction() adapts a function to a Netlify Function handler.

Install

npm install extrovert

Framework-free actions

Group your actions however you like. Each leaf is a factory that receives shared services and returns the function your application calls:

const actions = {
  users: {
    getUser: ({ db }) => async({ id }) => db.users.findById(id),
    createUser: ({ db }) => async({ name }) => db.users.create({ name })
  }
};

applySpec() wires in the dependencies while preserving the shape of the object:

const { applySpec } = require('extrovert');

const api = applySpec(actions, { db });
const user = await api.users.getUser({ id: '42' });

The resulting api is still an object of plain functions. Nothing ties it to HTTP. When you want an HTTP API, pass that same object to the Express adapter:

const express = require('express');
const { applySpec, objectRouter, toRoute } = require('extrovert');

const api = applySpec(actions, { db });
const app = express();

app.use(express.json());
app.use('/api', objectRouter(api, toRoute));

This creates routes for all four supported methods at each function path:

GET|PUT|POST|DELETE /api/users/getUser
GET|PUT|POST|DELETE /api/users/createUser

An OPTIONS handler is also added to each function path.

Express adapters

toRoute(routeHandler)

toRoute() adapts a plain action to Express and calls it as routeHandler(params, req, res). Most actions only need the first argument. The original request and response are available for cases that need lower-level control.

The adapter builds params by merging, in increasing precedence:

  1. The authorization header and headers prefixed with param- (with the prefix removed)
  2. req.query
  3. req.body
  4. req.params
  5. req._internals

Return a string to call res.send(), return any other value to call res.json(), or return null/undefined after sending a response yourself. Errors use error.status as the HTTP status, defaulting to 500.

An async iterable is written as a server-sent event stream:

const { toRoute } = require('extrovert');

app.get('/events', toRoute(async function* events() {
  yield 'connected';
  yield { count: 1 };
}));

objectRouter(object, transformRoute, options)

objectRouter() recursively maps object keys to URL path segments and function values to routes. transformRoute receives (fn, fullPath) and must return an Express request handler. It defaults to the identity function.

Use formatRoutePath to customize path segments:

const router = objectRouter(api, toRoute, {
  formatRoutePath: key => `/${key.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`
});

Passing null or undefined as the object returns null.

pureMiddleware(fn)

Use pureMiddleware() for middleware that only needs the request and returns a promise:

const { pureMiddleware } = require('extrovert');

app.use(pureMiddleware(async req => {
  req._internals = { user: await loadUser(req) };
}));

The wrapper calls next() when the promise fulfills and next(error) when it rejects.

Netlify adapter

The same style of plain action can run as a Netlify Function. Export the object returned by toNetlifyFunction():

const { toNetlifyFunction } = require('extrovert');

module.exports = toNetlifyFunction(async function getUser(params, event) {
  return db.users.findById(params.id);
});

The adapter combines param-* and authorization headers, query parameters, and a JSON request body into params. Successful results are JSON-encoded with status 200. Thrown errors produce status 500 with a JSON body containing message and extra, when present. All responses include an access-control-allow-origin: * header, and OPTIONS requests receive an empty 200 response.

Pass a service factory when the action itself needs to be created per invocation:

module.exports = toNetlifyFunction(
  services => params => services.users.findById(params.id),
  async () => ({ users: await connectToUsers() }),
  'getUser'
);

When the request accepts text/event-stream and the action returns an async iterable, the response body is an async iterable of server-sent event chunks.

applySpec(spec, ...args)

applySpec() is the dependency-wiring piece of Extrovert. It recursively calls every function in spec with the supplied arguments and returns an object with the same shape. Properties attached to each factory function are copied onto its result.

const { applySpec } = require('extrovert');

const spec = {
  users: services => ({
    findById: id => services.db.users.findById(id)
  })
};

const api = applySpec(spec, { db });
await api.users.findById('42');

License

Apache-2.0