pingdotgg / zact

Nothing to see here
https://zact-example.vercel.app
MIT License
983 stars 16 forks source link

Pattern to use middlewares #24

Open KaviiSuri opened 1 year ago

KaviiSuri commented 1 year ago

I'm really excited about what this package does, but middlwares/auth etc are something we don't have patterns for yet as it's very early.

What do you think of defining "middleware stacks" once and reusing them, something like "protectedProcedure" in tRPC.

Imagine zact taking in a list of functions to execute before the action as a stack, and the stack is reusable.

It'll just chain predefined function calls together to give you a typesafe middleware stacks

notadamking commented 1 year ago

Came up with a solution based on ExpressJS middleware: https://github.com/notadamking/exzact

Open to feedback (@KaviiSuri @t3dotgg)

notadamking commented 1 year ago

Here's a simple example using exzact:

import { z } from "zod"
import { exzact, Middleware } from "exzact"

interface Context {
  thing?: string
  something?: string
}

const app = exzact<Context>({
  thing: "things",
})

const logMiddleware: Middleware = async (context, next) => {
  console.log("Log Middleware (Pre): ", context)
  await next()
  console.log("Log Middleware (Post): ", context)
}

export const hello = app.zact(z.object({ stuff: z.string().min(1) }), {
  something: "stuff",
})(async ({ stuff }, { thing, something }) => {
  console.log(`Hello ${stuff}, you injected ${thing} and ${something}`)
})

app.use(logMiddleware)

hello({ stuff: "world" })

In the above example, the logging middleware will run before the action has executed.

The simple example above will log the following:

Log Middleware (Pre):  { input: { stuff: 'world' }, thing: 'things', something: 'stuff' }
Log Middleware (Post):  { input: { stuff: 'world' }, thing: 'things', something: 'stuff' }
Hello world, you injected things and stuff

There's also an example using Upstash (or local memory) for rate-limiting: https://github.com/notadamking/exzact/blob/master/examples/upstash.ts

As well as an example for using JWT for authentication (just an example, not secure, needs secret verification): https://github.com/notadamking/exzact/blob/master/examples/auth.ts

There are also multiple examples of adding middleware and context only to specific routes.

KaviiSuri commented 1 year ago

Wow, looks amazing, I was thinking of hacking around on it when I get time, but this looks perfect.

Do you think it'd be better if we didn't call it an app? But rather a better name protectedAction, loggedAction, adminAction, ratelimitedAction etc, this would allow us to share the middleware stack across the application in a clean, obvious way. Thoughts?

notadamking commented 1 year ago

Wow, looks amazing, I was thinking of hacking around on it when I get time, but this looks perfect.

Do you think it'd be better if we didn't call it an app? But rather a better name protectedAction, loggedAction, adminAction, ratelimitedAction etc, this would allow us to share the middleware stack across the application in a clean, obvious way. Thoughts?

Perhaps, though it really depends on how you intend to use the library. It was originally written to be used like an express app, where an app is a collection of actions. You can combine multiple middleware across actions however needed, or add middleware to the top-level app to add them to all actions. However, nothing stops you from using the library as you mention, and in fact, you may get better type support for it depending on the use case. For example:

import { z } from "zod"
import { ZactAction } from "zact/server"
import { ActionType, Middleware, exzact } from "exzact"

import { AuthenticatedMiddlewareContext } from "./middleware/context"
import { authMiddleware } from "./middleware"

export const protectedApp = exzact<AuthenticatedMiddlewareContext>()

const authInput = z.object({ userId: z.string().min(1) })
type AuthInput = typeof authInput

export function protectedAction<
  InputType extends z.ZodTypeAny,
  ContextType extends AuthenticatedMiddlewareContext
>(validator?: InputType, defaultContext: ContextType = {} as ContextType) {
  return function <RType = void>(
    action: ActionType<InputType, AuthenticatedMiddlewareContext, RType>,
    ...additional: Middleware<AuthenticatedMiddlewareContext>[]
  ): ZactAction<InputType & AuthInput, RType> {
    return protectedApp.zact(validator, defaultContext)(
      action,
      authMiddleware,
      ...additional
    )
  }
}

Then you can simply create a new protectedAction with:

export const authorize = protectedAction(
  z.object({ someAdditionalParam: z.string().min(1) })
)(async (input, { user }) => {
 .... input.userId is available here ...
})

The above protectedAction would work as you mention, and would be a nice DX with full type support. Perhaps I could add first-class support for this sort of thing to the library.