JSPM

  • Created
  • Published
  • Downloads 36
  • Score
    100M100P100Q39999F
  • License MIT

A next-gen middleware engine built for the WinterCG environments.

Package Exports

  • midwinter
  • midwinter/routing
  • midwinter/validation

Readme

Midwinter.js

Midwinter.js is a powerful, experimental middleware engine which allows middleware to define imperative code, metadata and type information, enabling a much smarter plugin system.

It is specifically built for WinterCG runtimes, hence the name.

npm i midwinter

Overview

Most middleware is purely imperative. To understand what middleware does, we must either manually trace the code, or actually run the code itself.

Midwinter enables middleware to declare it's effects via metadata (for static information) and types (for how the request context changes over time).

This simple – but very powerful – paradigm means plugins (like routing, validation) can truly extend upon the simple core of Midwinter, as opposed to merely hacking atop it like traditional middleware.

Basic Usage

const mid = new Midwinter();

const withAuth = mid.define(
  (req, ctx) => {
    // Partially update the request context
    return { userId: "123" };
  },
  { isAuthed: true } // Optionally provide meta
);

const handler = mid.use(withAuth).end((req, ctx) => {
  return Response.json({ userId: ctx.userId });
});

handler.meta.isAuthed;
// true

const response = await handler(new Request(opts));
// Handler is a standard request handler
// (req: Request) => Promise<Response>

Table of Contents

  1. Introduction

  2. Plugins

    2.1. Official Plugins

    ..2.1.1. Validation

    ..2.1.2. Routing

  3. API Reference

Introduction

How does Midwinter work?

With Midwinter, everything is middleware and all middleware can optionally provide clues about how it modifies our application. For changes that occur during a request, middleware can inform us how it might change the request context. For changes to the application as a whole, we can store metadata, which can later be used by other tooling.

With middleware providing an implementation, type information and static metadata, our app becomes much smarter and more flexible.

Concept Description
Request Context The request context represents how our app changes over the lifetime of a request. Using Midwinter, the changes to this context are automatically inferred, and can be explicitly defined if necessary.
Metadata Middleware can also register information that doesn't depend on the request lifecycle, in the form of metadata. For example, a given middleware could provide metadata about OpenAPI validation schema, to trivially enable client-side types.
Chaining Midwinter makes heavy use of chaining and TypeScript inference which helps to compose complex middleware pipelines using simple code, and to string our applications together at the type-level to surface issues before running anything, like trying to access missing data from the request context.

Plugins

Plugins are merely some combination of middleware and/or functionality which operates on a middleware chain.

For example, an OpenAPI plugin might add metadata via middleware and then "read" the app's routes to construct an OpenAPI spec.

The source code for the official plugins may be a good place to look to get started.

Official Plugins

The core of Midwinter is incredibly simple and straightforward, and so most functionality one might expect is actually implemented in plugins.

Below are the official plugins, which could just as easily be replaced by third-party alternatives.

Validation

Routing

API Reference

The API surface is intentionally very concise, comprised of only three methods.

.define(middleware)

Define a reusable middleware

const mid = new Midwinter();

// Define a simple auth middleware
const withAuth = mid.define((req, ctx) => {
  if (isAuthed) {
    return { user: { id: "123" } }; // Return ctx updates
  }

  // Early return with a response
  return Response.json({ error: true });
});

// Manually specify the context updates type
const withMutation = mid.define<{ wasMutated: boolean }>((req, ctx) => {
  ctx.wasMutated = true;
});

// Reuse chained middleware
const withAuthAndMutation = withAuth.use(withMutation);

.use(middleware)

Use a predefined or inline middleware

const getPostMid = mid
  .use(withAuth) // Use predefined middleware here

  // Or do it inline
  .use((req, ctx) => {
    return { userId: user.id };
  })

  // Or mutate the ctx, retaining type-safety
  .use<{ wasMutated: boolean }>((req, ctx) => {
    ctx.wasMutated = true;
  });

.end(middleware)

Use a middleware that must return a response

// Respond to the request and terminate the `.use` chain
const getPost = mid
  // ...snip
  .end((req, ctx) => {
    return new Response();
  });

// Use the request handler however you want
const response = await getPost(new Request(url));