{"id":23948588,"url":"https://github.com/sinclairnick/midwinter.js","last_synced_at":"2025-02-24T08:17:13.706Z","repository":{"id":271205682,"uuid":"912702173","full_name":"sinclairnick/midwinter.js","owner":"sinclairnick","description":"A next-gen middleware engine built for WinterCG environments.","archived":false,"fork":false,"pushed_at":"2025-02-11T19:32:54.000Z","size":184,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-11T20:30:06.564Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sinclairnick.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-06T08:10:28.000Z","updated_at":"2025-02-11T19:32:58.000Z","dependencies_parsed_at":"2025-02-11T20:36:46.301Z","dependency_job_id":null,"html_url":"https://github.com/sinclairnick/midwinter.js","commit_stats":null,"previous_names":["sinclairnick/midwinter.js"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Fmidwinter.js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Fmidwinter.js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Fmidwinter.js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Fmidwinter.js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sinclairnick","download_url":"https://codeload.github.com/sinclairnick/midwinter.js/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240441953,"owners_count":19801793,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-06T10:29:38.904Z","updated_at":"2025-02-24T08:17:13.670Z","avatar_url":"https://github.com/sinclairnick.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ccenter\u003e\n\n# ❄️ Midwinter.js\n\n_Middleware + WinterCG = Midwinter_\n\n\u003c/center\u003e\n\nMidwinter is a plugin-based middleware engine used to build HTTP backend applications.\n\nThe main innovation driving Midwinter is enabling middleware to declare _metadata_. This enables powerful introspection and static analysis, which in turn enables an infinitely flexible plugin system.\n\nThe plugin system is so powerful that even core functionality like routing can happily exist as a plugin.\n\n```sh\nnpm i midwinter\n```\n\n\u003e [!IMPORTANT]  \n\u003e Midwinter is currently in beta status. It won't be fundamentally overhauled but may experience some breaking API changes. That said, it is currently used in production.\n\n## Motivation\n\nWhen we add middleware to our applications, it might change a `req` object, add some routes, or something else entirely. As we build up an increasingly complex web of routes, each piece of middleware becomes impossible to track and is essentially a black box.\n\nTo improve this situation, we can inform both static and runtime environments as to how our application behaves, in the form of _types_ and _metadata_, respectively. In doing so, we can offload fundamental functionality to plugins; programmatically introspect and understand our applications; and trace how our request context changes over time, via TypeScript.\n\n## Basic Usage\n\n```ts\nconst withAuth = new Midwinter({\n  requiresAuth: true, // Define metadata (optional)\n}).use((req, ctx) =\u003e {\n  return { userId: \"123\" }; // Add data to request context\n});\n\nconst getUser = new Midwinter()\n  .use(withAuth) // Apply middleware\n  .end((req, ctx) =\u003e {\n    const { userId } = ctx;\n\n    return Response.json({ userId });\n  });\n\nconst response = await getUser(new Request(/*...*/));\n\nawait response.json(); // { userId: \"123\" }\n\ngetUser.meta.requiresAuth; // true\n```\n\n## Table of Contents\n\n- [Guide](#guide)\n  - [Getting Started](#getting-started)\n  - [Return value behaviour](#return-value-behaviour)\n  - [Request Context](#request-context)\n  - [Listening to responses](#listening-to-responses)\n  - [Chaining](#chaining)\n  - [`.end`ing pipelines](#ending-pipelines)\n  - [Metadata](#metadata)\n- [Plugins](#plugins)\n  - [Official Plugins](#official-plugins)\n  - [Routing](#routing)\n  - [Validation](#validation)\n  - [Cors](#cors)\n  - [Client Types](#client-types)\n\n### Key Concepts\n\n\u003cdetails\u003e\n\u003csummary\u003e\n\u003cb\u003eRequest Context\u003c/b\u003e\n\u003c/summary\u003e\n\nThe 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.\n\n\u003e _e.g. determining the current user and adding to the request context_\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n\u003cb\u003eMetadata\u003c/b\u003e\n\u003c/summary\u003e\n\nMiddleware 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.\n\n\u003e _e.g specifying the path, method or validation schema for a request handler, for later use with a routing or validation plugin._\n\n\u003c/details\u003e\n\n## Guide\n\nThe following is the entire Midwinter API:\n\n```ts\nconst handle = new Midwinter(meta)\n  //\n  .use(middleware)\n  //\n  .end(endMiddleware);\n```\n\nMidwinter is remarkably simple and deceptively powerful. With only this API, we can create complex middleware pipelines and defer much of what might exist in a framework to plugins instead, without any loss of _functionality_ or _ergonomics_.\n\n\u003e [!NOTE]  \n\u003e These docs are currently a work in progress. They are mostly there but may hve the odd gap or minor error. Please raise an issue if you run into anything.\n\n### Getting Started\n\nAt it's simplest, middleware can be a regular function.\n\n```ts\nconst isAuthed = async (req: Request) =\u003e {\n  const user = await getUser(req);\n\n  if (user == null) {\n    throw new Error(\"Unauthorized!\");\n  }\n};\n```\n\nThis can then be used to create a basic **middleware pipeline**.\n\n```ts\nconst handleRequest = new Midwinter()\n  .use(isAuthed) // \u003c--\n  .end(() =\u003e Response.json({ ok: true }));\n```\n\nWhen we `.use` a middleware, we are registering it to a middleware **pipeline**. To actually invoke this middleware pipeline, we need to `.end` it, returning a **request handler** function.\n\n```ts\n// Defining and registering middleware\nconst middleware = new Midwinter().use(() =\u003e {});\n\n// Ending a pipeline\nconst handle = middleware.end(() =\u003e new Response(/**...*/));\n\n// Executing the request handler\nconst response = await handle(new Request(/**... */));\n```\n\nWe can chain middleware together into reusable pipelines.\n\n```ts\nconst one = new Midwinter().use(() =\u003e {\n  console.log(1);\n});\n\nconst two = new Midwinter().use(() =\u003e {\n  console.log(2);\n});\n\nconst three = () =\u003e {\n  console.log(3);\n};\n\nconst withOneTwoThree = one.use(two).use(three);\n```\n\nThe above example demonstrates the three ways middleware can be defined/registered:\n\n1. **Extending** from an existing pipeline\n2. Applying a **pipeline** via `.use`\n3. Applying a **function** via `.use`\n\nIn other words, **instances of Midwinter** can also be treated as middleware itself!\n\nWhen this pipeline is `.use`d or extended, the middleware is run in sequence.\n\n```ts\nconst handle = withOneTwoThree.end();\n\nhandle(new Request(/**... */));\n// 1\n// 2\n// 3\n```\n\nWhen our middleware returns an **object**, it gets shallowly merged with the existing **request context**. This context is passed as the second parameter to any middleware functions.\n\n```ts\nconst withReqId = new Middleware().use((req) =\u003e {\n  return { id: req.headers.get(\"x-request-id\") };\n});\n\nconst withLogReqId = withReqId.end((req, ctx) =\u003e {\n  console.log(ctx.id);\n});\n```\n\nWe can also specify **metadata** to make our app more informative to **both humans and computers**.\n\n```ts\nconst withPath = \u003cT extends string\u003e(path: T) =\u003e {\n  return new Midwinter({ path });\n};\n\nconst middleware = new Midwinter().use(withPath(\"/users/:id\")).end();\n\nmiddleware.meta.path === \"/users/:id\";\n// The `meta.path` type is also `/users/:id`\n```\n\nSo far, we've only been intercepting the _request_. But we can also intercept and modify the _response_. By returning a function, we can register response middleware.\n\n```ts\nconst withTiming = new Midwinter().use(() =\u003e {\n  const start = Date.now();\n\n  return (res: Response) =\u003e {\n    const headers = new Headers(res.headers);\n\n    headers.set(\"x-timing\", String(Date.now() - start));\n\n    return new Response(res.body, {\n      status: res.status,\n      statusText: res.statusText,\n      headers,\n    });\n  };\n});\n```\n\n---\n\nWe have seen how Midwinter is fairly simple, but these trivial examples hardly show how it is _powerful_. To dive deeper into how Midwinter works, continue on below. To get a better sense of how this paradigm can enable interesting plugins, continue to the [Plugins](#plugins) section.\n\n### Return value behaviour\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nMiddleware often needs to update the request context,return early responses and observe/modify outbound responses. Midwinter achieves this using a functional style, relying on the value returned by a middleware function.\n\n|              |                                                                              |\n| ------------ | ---------------------------------------------------------------------------- |\n| **Object**   | Update request context (shallowly)                                           |\n| **Function** | Registers a \"response listener\" (outbound middleware)                        |\n| **Response** | Return the response, passing through any response listeners defined upstream |\n\n\u003e Note that response listeners are executed in _reverse_ or \"inside-out\" order\n\nOn top of being convenient and simple, relying on return type maintains type-safety across our middleware pipeline by simply inferring what request context updates our middleware makes. In turn, we can avoid entire classes of errors and work, knowing what data does (not) exist at a given point.\n\nThese three options look something like:\n\n```ts\nnew Midwinter().use(() =\u003e {\n  if (withUpdate) {\n    return { foo: \"bar\" };\n  }\n\n  if (withResponse) {\n    return Response.json({ early: true });\n  }\n\n  return (res: Response) =\u003e {\n    // Optionally return a modified response\n    return new Response(res.body, {\n      status: 301,\n    });\n  };\n});\n```\n\n\u003c/details\u003e\n\n### Request Context\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nThe second argument Midwinter passes to any middleware is the request context.\n\n```ts\nconst withIp = new Midwinter().use(() =\u003e ({ ip: \"123\" }));\n\nconst ipLogger = withIp.use((req, ctx) =\u003e {\n  console.log(ctx.ip);\n});\n```\n\n#### Updating the request context\n\n##### Via return value\n\nWe can return a simple JavaScript object to indicate a context update.\n\n```ts\nconst withReqTime = mid.define((req, ctx) =\u003e {\n  return { start: new Date() };\n});\n\nnew Midwinter()\n  .use(withReqTime) //\n  .use((req, ctx) =\u003e {\n    ctx.start != null; // true\n  });\n```\n\n##### Via mutation\n\nMuch of the time, we only need to return one of the above three return value possibilities: object, function or response.\n\nWhile returning both a response and response listener is redundant, we may still want to _update the request_ context during these two cases. To do so, we can mutate the request context directly.\n\n```ts\nconst withReqTime = new Midwinter().use\u003c{ start: number }\u003e((req, ctx) =\u003e {\n  const start = Date.now()\n\n  // Update context\n\tctx.start = start\n\n\n\t// Returning a response listener\n  return () =\u003e {\n    const end = Date.now()\n\n\t\tconsole.log(\"Took\" start - end)\n  };\n});\n```\n\nIn this example, the type has been explicitly provided, which maintains type-safety. However, this is entirely optional.\n\n\u003c/details\u003e\n\n### Listening to responses\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nReturning a function enables listening to and modifying the outbound response. The function takes a single argument: the response object.\n\n```ts\nnew Midwinter().use(() =\u003e {\n  return (res: Response) =\u003e {\n    // TODO: Return a new response... or not\n  };\n});\n```\n\nWe can modify the response object by returning a new one. Bear in mind both `Request` and `Response`, as per the WinterCG standard, are _immutable_, so you should make use of the [`.clone()`](https://developer.mozilla.org/en-US/docs/Web/API/Response/clone) method when applicable.\n\n\u003c/details\u003e\n\n### Chaining\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nMidwinter 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.\n\n```ts\nconst withAuth = new Midwinter().use(/**... */);\nconst isAdmin = withAuth.use(/**... */);\nconst isSuperAdmin = isAdmin.use(/**... */);\n\nnew Midwinter().use(isSuperAdmin);\n```\n\nBy chaining we can extend middleware pipelines easily and create complex pathways for a request to travel through.\n\n\u003e Each time we chain middleware a _new instance_ is returned, meaning each pipeline is independent and changing one will not change any others.\n\n\u003c/details\u003e\n\n### `.end`ing pipelines\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nTo officially _end_ a middleware pipeline, and thus return a request handler we can run, we use the `.end` method. This is is similar to `.use`, but _must_ return a `Response` object.\n\nInstead of a new `Midwinter` instance being returned, `.end` results in a request handler function that accepts a `Request` and returns a `Response` promise.\n\n```ts\nconst handle = new Midwinter()\n  .use(withA)\n  .use(withB)\n  .use(withC)\n  .end((req, ctx, meta) =\u003e {\n    return Response.json({ ok: true });\n  });\n\n// Invoke the handler like:\nconst response = await handle(request);\n```\n\n\u003c/details\u003e\n\n### Metadata\n\nMetadata enables middleware to \"decorate\" our backend apps in powerful ways that traditional middleware can't.\n\n```ts\nconst withName = (name: string) =\u003e {\n  const meta = { name };\n\n  return new Midwinter(\n    meta // \u003c--\n  );\n};\n```\n\nIn this case, our \"middleware\" actually runs nothing at all - it is _only_ metadata. If we `.use` this middeware, our resulting request handler will possess this metadata, which can then be utilised by other tooling to great effect.\n\n```ts\nconst handle = new Midwinter()\n  .use(withName(\"getUser\")) // \u003c--\n  .end(() =\u003e {\n    // ...\n  });\n\nhandle.meta.name === \"getUser\";\n```\n\nThis approach to metadata enables plugins, or own code, to fully \"see\" our app, both programmatically and type-wise. This feature enables a very simple and powerful paradigm for plugins.\n\n#### Metadata merging\n\nLike request context updates, metadata is shallowly merged.\n\n```ts\nconst handle = new Midwinter()\n  .use(withName(\"getUser\")) // \u003c--\n  .use(withName(\"getPost\")) // \u003c--\n  .end(() =\u003e {\n    // ...\n  });\n\nhandle.meta.name === \"getPost\";\n```\n\n## Plugins\n\nIn Midwinter, \"plugins\" (as opposed to regular middleware), broadly refers to a set of interacting middleware, or functionality that operates on request handlers.\n\nFor example, a plugin might add some metadata to a middleware pipeline and then access that metadata elsewhere, down the line.\n\n### Official Plugins\n\nMidwinter itself is a very slim middleware pipeline, and so most \"app stuff\" is provided by plugins.\n\nCore app functionality like routing and validation (among others) are provided. However, even these can be replaced by third party alternatives, without much downside.\n\n\u003e More official plugins will be added in coming months. Feel free to open a PR for any requests.\n\n### Routing\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nRouting is central to any backend app. However, with the advent of full-stack frameworks and file-system routing, not all apps _need_ an explicit router.\n\nFor those who do, this plugin enables a flexible routing solution.\n\nIn short, the routing plugin turns a list of request handlers into an actual \"app\".\n\n#### Setup\n\n```ts\nimport * as Routing from \"midwinter/routing\";\n\nexport const { router, route } = Routing.init(opts);\n```\n\n#### `routing`\n\nThe `routing` function is a middleware that adds routing-related metadata to a route.\n\n```ts\nconst handle = new Midwinter()\n  .use(\n    route({\n      path: \"/user\",\n      method: \"/get\",\n    })\n  )\n  .end(() =\u003e {\n    // ...\n  });\n```\n\nTo more easily group routes, while avoiding duplication, we can create a prefixed route utility like so.\n\n```ts\nconst apiRoute = prefixed(\"/api/v1\")\n\nconst getPost = new Midwinter()\n\t.use(apiRoute({\n\t\tpath: \"/post/:id\",\n\t\tmethod: \"get\n\t}))\n\t.end(() =\u003e {\n\t\t// ...\n\t})\n\ngetPost.meta.path // /api/v1/post/:id\n```\n\n#### `router`\n\nTo actually instantiate our app router, we use the `router` function.\n\n```ts\nconst app = router([getPost, ...others], opts);\n\n// Call app router on new request:\nconst response = await app(request);\n```\n\n\u003c/details\u003e\n\n### Validation\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nInput and output validation is table stakes for any serious backend app. While we can always imperatively validate data, the validation plugin enables a declarative API that is more concise.\n\nBy virtue of being declarative, the validation plugin also facilitates opportunities for easily inferring types or generating specs like OpenAPI.\n\nThe validation plugin works with most popular schema validation libraries out of the box. The below examples are using `zod`.\n\n#### Setup\n\n```ts\nimport * as Validation from \"midwinter/validation\";\n\nexport const { valid, validLazy, output } = Validation.init(opts);\n```\n\n#### `valid`\n\nThe `valid` function enables validating various input and outputs of your backend.\n\n```ts\nconst Schema = z.object({\n  // ...\n});\n\nnew Midwinter()\n  .use(\n    valid({\n      // All possible fields:\n      Query: Schema,\n      Params: Schema,\n      Headers: Schema,\n      Body: Schema,\n      Output: Schema,\n    })\n  )\n  .end((req, ctx) =\u003e {\n    const { query, params, headers, body } = ctx;\n  });\n```\n\nWith `valid`, all components are pre-parsed and added to the context.\n\n\u003e Note that `Output` _does not_ parse the response.body. See below.\n\n#### `validLazy`\n\nIn contrast to `valid`, `validLazy` does not pre-parse anything. Instead a parsing function is added to the context, offering greater flexibility over how parsing is handled and what to parse.\n\n```ts\nnew Midwinter()\n  .use(\n    validLazy({\n      // ...same options\n    })\n  )\n  .end(async (req, ctx) =\u003e {\n    const { parse } = ctx;\n\n    // Parse a single part\n    const query = await parse(\"query\");\n\n    // or parse all parts\n    const { query, params, body, headers } = await parse();\n  });\n```\n\n#### `output`\n\nAt a minimum, the `output` function allows us to return a value, which will then get packaged into a JSON response.\n\nIf we've specified an `Output` schema, our return value will be validated against this.\n\n```ts\nmid\n  .use(\n    valid({\n      Output: z.object({ foo: z.string() }), // \u003c-- Optionally specify output schema here\n    })\n  )\n  .end(\n    output((req, ctx) =\u003e {\n      return { foo: \"bar\" }; // \u003c-- Will be parsed and returned as a JSON Response\n    })\n  );\n```\n\n\u003c/details\u003e\n\n### Cors\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nTODO: Add docs\n\n```ts\nimport * as Cors from \"midwinter/cors\";\n\nexport const { cors } = Cors.init();\n\nnew Midwinter().use(cors(opts));\n```\n\n\u003c/details\u003e\n\n### Client Types\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nTODO: Add docs\n\n```ts\nimport * as ClientTypes from \"midwinter/client-types\";\nimport type { AppRoutes } from \"./app\";\n\nexport type AppDef = ClientTypes.InferApp\u003ctypeof AppRoutes\u003e;\n```\n\n\u003c/details\u003e\n\n### Open Telemetry\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand\u003c/summary\u003e\n\nTODO: Add docs\n\n```ts\nimport * as Otel from \"midwinter/otel\";\n\nconst { otel } = Otel.init();\n\nnew Midwinter().use(otel());\n```\n\n\u003c/details\u003e\n\n### OpenAPI\n\n\u003e Coming soon\n\n...\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsinclairnick%2Fmidwinter.js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsinclairnick%2Fmidwinter.js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsinclairnick%2Fmidwinter.js/lists"}