Skip to content
This repository has been archived by the owner on Sep 27, 2023. It is now read-only.

Pattern to use middlewares #24

Open
KaviiSuri opened this issue May 9, 2023 · 4 comments
Open

Pattern to use middlewares #24

KaviiSuri opened this issue May 9, 2023 · 4 comments

Comments

@KaviiSuri
Copy link

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
Copy link

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

Open to feedback (@KaviiSuri @t3dotgg)

@notadamking
Copy link

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
Copy link
Author

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
Copy link

notadamking commented May 26, 2023

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants