#760·elysia

Allow multiple handlers with same path in something like guard

Author: bogeychanCreated Jul 27, 2024Updated Sep 12, 2026
Labelsenhancementfeature request

What is the problem this feature would solve?

Webhooks send POST requests to the same endpoint (based on event-literal). Someone on discord wanna separate this logic into different functions based on event.

Pseudo code:

typescript
const customerCreated = new Elysia({ name: "created" })
  .if(({ body }) => body.event === "customer.created")
  .post("/", () => console.log("customer created!"));

const customerUpdated = new Elysia({ name: "updated" })
  .if(({ body }) => body.event === "customer.updated")
  .post("/", () => console.log("customer updated!"));

const stripeWebhook = new Elysia().use(customerCreated).use(customerUpdated);

What is the feature you are proposing to solve the problem?

Allow something like this to work with the same path (/):

typescript
const webhooks = new Elysia({ name: "webhooks" })
  .guard(
    {
      body: t.Object({
        event: t.Literal("customer.created"),
        propCreated: t.String(),
      }),
    },
    (app) => app.post("/", () => "cr")
  )
  .guard(
    {
      body: t.Object({
        event: t.Literal("customer.updated"),
        propUpdated: t.String(),
      }),
    },
    (app) => app.post("/", () => "up")
  );

new Elysia().use(webhooks).listen(8080);

At the moment it throws a validation error:

Click to expand
json
{
  "type": "validation",
  "on": "body",
  "summary": "Property 'propUpdated' is missing",
  "property": "/propUpdated",
  "message": "Required property",
  "expected": {
    "event": "customer.updated",
    "propUpdated": ""
  },
  "found": {
    "event": "customer.created"
  },
  "errors": [
    {
      "type": 45,
      "schema": {
        "type": "string"
      },
      "path": "/propUpdated",
      "message": "Required property",
      "summary": "Property 'propUpdated' is missing"
    },
    {
      "summary": "Expected 'customer.updated'",
      "type": 32,
      "schema": {
        "const": "customer.updated",
        "type": "string"
      },
      "path": "/event",
      "value": "customer.created",
      "message": "Expected 'customer.updated'"
    },
    {
      "type": 54,
      "schema": {
        "type": "string"
      },
      "path": "/propUpdated",
      "message": "Expected string",
      "summary": "Expected  property 'propUpdated' to be  string but found: undefined"
    }
  ]
}

If i send this body:

json
{
    "event": "customer.created",
    "propCreated": "1"
}

What alternatives have you considered?

I considered this, but it has a lot of unnecessary code:

This is not easy for newbies and i've to do isCustomerUpdated , ... checks because of typescript type guard limitation with nested unions.

Click to expand
typescript
import {
  t,
  Elysia,
  type Static,
  type Context,
  type TSchema,
  type InputSchema,
} from "elysia";

const customerCreated = t.Object({
  event: t.Literal("customer.created"),
  propCreated: t.String(),
});
type CustomerCreatedContext = StaticContextExclude<
  typeof customerWebhooks,
  Static<typeof customerUpdated>
>;

const customerUpdated = t.Object({
  event: t.Literal("customer.updated"),
  propUpdated: t.String(),
});
type CustomerUpdatedContext = StaticContextExclude<
  typeof customerWebhooks,
  Static<typeof customerCreated>
>;

const customerWebhooks = {
  body: t.Union([customerCreated, customerUpdated]),
} satisfies InputSchema;

type StaticContext<T> = Context<{
  [Key in keyof T]: T[Key] extends TSchema ? Static<T[Key]> : T[Key];
}>;

type StaticContextExclude<T, E> = Context<{
  [Key in keyof T]: T[Key] extends TSchema
    ? Exclude<Static<T[Key]>, E>
    : Exclude<T[Key], E>;
}>;

function handleCustomerCreated({ body }: CustomerCreatedContext) {
  return body.propCreated;
}

function handleCustomerUpdated({ body }: CustomerUpdatedContext) {
  return body.propUpdated;
}

function isCustomerCreated(
  ctx: StaticContext<typeof customerWebhooks>
): ctx is CustomerCreatedContext {
  return ctx.body.event === "customer.created";
}

function isCustomerUpdated(
  ctx: StaticContext<typeof customerWebhooks>
): ctx is CustomerUpdatedContext {
  return ctx.body.event === "customer.updated";
}

const webhooks = new Elysia({ name: "webhooks" }).post(
  "/",
  (ctx) => {
    if (isCustomerCreated(ctx)) {
      return handleCustomerCreated(ctx);
    } else if (isCustomerUpdated(ctx)) {
      return handleCustomerUpdated(ctx);
    }
  },
  customerWebhooks
);

new Elysia().use(webhooks).listen(8080);