-
Notifications
You must be signed in to change notification settings - Fork 63
Description
Perceived Problem
- users resort to
server.custom
in every app to add express middleware server.custom
should be a rare escape hatchserver.custom
widespread use would make Nexus framework de-facto coupled to express- plugins need to be able to add middleware, but they cannot
- no middleware that spans serverless and serverful
Idea
-
works like a FILO (first in last out) stack
-
beforeware can short-circuit, aftwares of beforewares that ran will run
-
await next
to transition seemlessly from beforeware to afterware -
return
to short-citcuit- in afterware has no effect, Nexus will log warning
-
third parameter is
ctx
, intentionally mimicks position in resolver sig -
mutations to ctx are visible in resolvers, super simple
- no more
schema.addToContext
- no more
-
plugins can extend req object for use at app level [1] via typegen [2]
- so a heroku plugin could add types for the req headers heroku provides and likewise for a now plugin for now headers.
-
plugins can extend res object for use at app level [1] via typegen [2]
-
plugins can extend ctx object [3]
-
everything you see here will work as well for a serverless deployment (lambda) as it will a serverful one (ec2)
-
req/res/ctx augmentation by plugins will be visible in app middleware types
-
app level augmentation of req/res/ctx in middleware will not be visible in later middleware since the type of middleware can be altered based on syntactic order.
server.middleware((req, res, ctx) => { ctx.foo = 1 }) server.middleware((req, res, ctx) => { ctx.foo }) // type check error, but actually correct
-
app level augmentation of ctx in middleware will be visible in resolver sigs
-
does not handle errors. Errors are fatal and handled by the framework. Approach akin to fastify
[1] "for use at app level" because we don't want plugins to start depending on features from other plugins, or do we?
[2] We cannot use global interface merging b/c we don't know if the plugin will be enabled or disabled. We resort to typegen.
[3] They already can, but ctx is only for resolvers right now. This point is saying that plugins will be able to modify context for middleware too.
Ideas from other middleware systems we align with
koa:
however a key design decision was made to provide high level "sugar" at the otherwise low-level middleware layer. This improves interoperability, robustness, and makes writing middleware much more enjoyable.
fastify
Uncaught errors are likely to cause memory leaks, file descriptor leaks and other major production issues. Domains were introduced to try fixing this issue, but they did not. Given the fact that it is not possible to process all uncaught errors sensibly, the best way to deal with them at the moment is to crash. In case of promises, make sure to handle errors correctly.
Fastify follows an all-or-nothing approach and aims to be lean and optimal as much as possible. Thus, the developer is responsible for making sure that the errors are handled properly. Most of the errors are usually a result of unexpected input data, so we recommend specifying a JSON.schema validation for your input data.
Note that Fastify doesn't catch uncaught errors within callback-based routes for you, so any uncaught errors will result in a crash. If routes are declared as async though - the error will safely be caught by the promise and routed to the default error handler of Fastify for a generic Internal Server Error response. For customizing this behaviour, you should use setErrorHandler.
Examples
Beforeware
server.middleware((req, res, ctx) => {
const token = extractToken(req)
ctx.user = {
id: token.claims.userId
}
})
Beforeware & Afterware
server.middleware(async (req, res, ctx, next) => {
const start = Date.now()
await next
const end = Date.now()
res.headers.set('x-response-time', end - start)
})
Beforeware short circuit
server.middleware((req, res, ctx) => {
const token = extractToken(req)
if (!token) {
res.status = 401
res.body = '...'
return res
}
ctx.user = {
id: token.claims.userId
}
})
plugin augmnet req
module {
interface NexusRequest {
/* extend type here */
foo: string
}
}
export default NexusPlugin.create(project => {
project.runtime((augment) => {
augment.request({
type: `
foo?: string
`,
term(req) {
if (math.random() > 0.5) {
req.foo = 'bar'
}
}
})
augment.response({
type: `
foo?: string
`,
term(res) {
if (math.random() > 0.5) {
res.foo = 'bar'
}
}
})
})
Lifecycle
imagine three middlewares
normal
before: middleware 1
before: middleware 2
before: middleware 3
graphql handler
after: middleware 3
after: middleware 2
after: middleware 1
short-circuit ex 1
before: middleware 1
before: middleware 2 RETURN
after: middleware 1
Notes
Alt: Dedicated before/afterware functions
- pro: can statically type that Res is only for beforeware
- con:
server.before((req, res, ctx) => {})
server.after((req, res, ctx) => {})
type Before = (req:Req, res:Res, ctx:Ctx) => Res
type After = (req:Req, res:Res, ctx:Ctx) => void
Plugin-level type extraction
We will run type-extraction at the app level. Why not the plugin level too then?
// before
export default NexusPlugin.create(project => {
project.runtime((augment) => {
augment.request({
type: `
foo?: string
`,
term(req) {
if (math.random() > 0.5) {
req.foo = 'bar'
}
}
})
})
// after
export default NexusPlugin.create(project => {
project.runtime((hooks) => {
hooks.onRequest(req => {
if (math.random() > 0.5) {
req.foo = 'bar'
}
})
})
Why not use an existing system
- Most are not optimized for TypeScript (exception: https://github.com/gcanti/hyper-ts)
- None are not optimized for TypeGen
- None have the concept of context that spans server middleware + resolvers
- None have the concept of being deployed to serverful or serverless environments
- None have the higher-order concept of how to handle being within a plugin, which brings an ordering challenge.
Links
- https://zeit.co/docs/v2/network/headers
- https://devcenter.heroku.com/articles/http-routing#heroku-headers
- https://koajs.com/
- https://github.com/koajs/koa/wiki#middleware
- https://github.com/gcanti/hyper-ts
- https://github.com/fastify/fastify/#fastifyusemiddlewarereq-res-next
- https://github.com/fastify/fastify/blob/master/docs/Errors.md
- https://expressjs.com/en/guide/using-middleware.html