enhance-dev/enhance

Global middleware ffs

Closed this issue · 3 comments

Currently we allow people to use "middleware" by supplying an Array of handlers. This is fine, but many users ( and Beginners ) have complained as well as come up with their own hand rolled solutions for when you need a handler; like auth, to be called for many route handlers.
Users need a way to add common state to responses in a way that does not feel repetitive or error prone.

It is time we add a blessed pattern for global middleware.

Proposals:

  1. We document how to import a collection of handlers into your route handler. This could use the existing array syntax.

Pros:

  • explicit
  • Does not require more than docs
  • Reuses existing patterns
  • Easy to learn

Cons:

  • Requires a user to manually import the correct set of handlers into every route that needs it
  • Exclusion
  • Requires understanding of shared code in Enhance projects
  • No prior art to lean on, new users have to learn a new way to middleware
  • Not super "Enhance-y"
  1. Introduce a middleware folder structure that mimics api and page handler patterns.
app
├── api ............... data routes
│   └── index.mjs
├── browser ........... browser JavaScript
│   └── index.mjs
├── elements .......... custom element pure functions
│   └── my-header.mjs
├── middleware ............... common response mutations
│   └── index.mjs
├── pages ............. file-based routing
│   └── index.html
└── head.mjs .......... custom <head> component

Pros:

  • Follows Enahnce conventions so it reinforces how apis and pages work
  • Easy to follow
  • less work for users as it matches existing routes. /* would be an acceptable global handler

Cons:

  • Dangerous as it is easy for users to slow their apps down accidentally
  • Exclusion
  • implicit-ish
  1. Add a middleware.js file to the root of the project
    Pros:
  • additive
  • simple
  • does not require as much code change
    Cons:
  • syntax would be complicated for path matching and order
  • Exclusion
  • no prior art

@tbeseda mentioned some prior art:

Going to miss Enhance sync today since I'll be on that extended lunch. wrt to middleware, I only planned to chime in with checking on prior art and seeing if we can crib something that feels good but isn't as complicated:
Like where does Next put theirs? https://nextjs.org/docs/app/building-your-application/routing/middleware
Express had devs declare middleware in a specific execution order, which is more powerful but not file-based and probably anti-Enhance
Rails is Rack middleware itself, so you'd just tap into the "Stack" of middleware in application.rb -- also not Enhance-y
Phoenix has "plug" but it's very OOP - basically decorators and also per-route I think

Here is my proposal: https://github.com/ryanbethel/enhance-middleware

Enhance Middleware

A middleware wrapper for the enhance html first framework.
Includes:

  • Global middleware handlers
  • Response for data passing between middleware

Experimental

This package is a work in progress.
The API will likely change.
The goal is to test ideas and patterns for middleware to support Enhance.

Getting Started

First install the package with npm i enhance-middleware.
Use this wrapper in API routes to give an optional response second argument.
This response has helper methods for passing data from one middleware to the next.
It is fully backward compatible with previous function signature.
This may be used together with old style API handlers.

Basic Usage

//app/api/index.mjs
import midWrap from 'enhance-middleware'
export const get = midWrap(one, two)

async function one (req,response) {
  response.json = { ...(response.json || {}), first:true }
}

async function two (req,response) {
  response.json = { ...(response.json || {}), second:true }
  return response
}
// { first:true, second:true }

Global Middleware Usage

//app/middleware/global-middleware.mjs
import {makeGlobalWrap} from 'enhance-middleware'
import otherStuff from './other-stuff.mjs'
const manifest = {
  '/': navData
  '/$$': [navData, accountData],
  '/special/route': otherStuff,
}

function navData(req,response){
  response.json = {...(response.json || {}), path:req.path }
}

function accountInfo(req, response) {
  const session = response.session
  response.json = {...(response.json || {}), authorized: session?.authorized ? session?.authorized : false }
}

export const globeWrap = makeGlobalWrap(manifest)
//app/api/index.mjs
import globeWrap from '../middleware/global-middleware.mjs'
export const get = globeWrap(one, two)

async function one (req,response) {
  response.json = {...(response.json || {}), first:true }
}

async function two (req,response) {
  response.json = {...(response.json || {}), second:true}
}
// { path:'/', authorized:{user:'janedoe'}, first:true, second:true }

global handler use cases:

  • highlighted nav
  • show avatar or not (when authenticated)

Of the first three proposals I think the third is what I prefer. If, like head.mjs, it has the req and state people would be free to implement the path matching however they want using req.path. The order dependence is a problem, although it could export two functions, first and last, that run before any API routes and after API routes. If they need more granularity to run middleware between other middleware then they should just add it in the API routes as regular middleware.