GitHub - tmjeee/deno-middleware · GitHub
Skip to content

tmjeee/deno-middleware

Repository files navigation

Deno Middleware

JSR JSR Score

Middleware functions / utilities for use with Supabase Deno http server.

Features

  • 🚀 Simple middleware function applyMiddleware(...) for Deno.serve(...) for Supabase Edge Functions
  • 📦 Common middleware out of the box eg. (corsMiddlewareFn(), httpMethodMiddlewareFn(...) etc.)
  • 🔒 Easy to add new middleware, handler functions (MiddlewareFn and HandlerFn type respectively)
  • ✨ TypeScript support out of the box
  • 🧪 Well tested (Hopefully !!!)
  • 📚 Fully documented (Hopefully !!!)

Installation

Deno

import { applyMiddleware } from "jsr:@tmjeee/deno-middleware";
import { applyMiddlewareWithSupabaseContext } from "jsr:@tmjeee/deno-middleware/v2";

Or add to your deno.json:

{
  "imports": {
    "@tmjeee/deno-middleware": "jsr:@tmjeee/deno-middleware@^0.2.1"
  }
}

Node.js (via JSR npm compatibility)

For Deno server running in Supabase Edge Functions only. Doesn't make sense to run it in Node.

Usage

Examples

Note

📢 For a complete practical example, see examples/basic.ts for Deno.serve(...) and examples/basic-v2.ts for export default { fetch: withSupabase(...) } which demonstrates a typical real-world usage with CORS, HTTP method validation, and body validation.

Simple usage

// NOTE: using old `Deno.serve(...)`
import { applyMiddleware } from "@tmjeee/deno-middleware";

Deno.serve(
  {
    port: 3000,
    hostname: "0.0.0.0",
    onListen({ port, hostname }) {
      console.log(`Server is running on http://${hostname}:${port}`);
    },
  },
  applyMiddleware({
    middlewares: [
      // custom middlewares
      (req, ctx, next) => {
        // your middleware
        if (some_condition) {
          // custom response
          return new Response(...)          
        }
        // else pass it to the next middleware
        return next(req, ctx);        
      }
      // more middlewares ...
    ],
    // your handler that handle the request after passing thorugh all middlewares
    handler: (req, ctx) => {
      return new Response(
        JSON.stringify({success: true, message: `Hello world`});
      )
    }
  }),
);

// NOTE: using  new `export default { fetch: withSupabase(...) } `
import { SupabaseContext, withSupabase } from "@supabase/server";
import { applyMiddlewareWithSupabaseContext } from "@tmjeee/deno-middleware/v2";
import { httpMethodWithSupabaseContextMiddlewareFn } from "@tmjeee/deno-middleware/v2";
import { zodValidateBodyWithSupabaseContextMiddlewareFn, ZodValidateBodyWithSupabaseContextMiddlewareContext } from "@tmjeee/deno-middleware/v2"; 
import z from 'zod';

export default {
  fetch: withSupabase(
    {
      auth: ['user'],
    },
    applyMiddlewareWithSupabaseContext({
      middlewares: [
        // custom middlewares
        (req, ctx, next) => {
          // your middleware 
          if (some_condition) {
            // custom resonse
            return new Response(...)
          }
          // esle pass it to the next middleware
          return next(req, ctx);
        }
        // more middlewares ...
      ],
      // your handler that handle the request after passing thorugh all middlewares
      // deno-lint-ignore require-await
      handler: async <T extends SupabaseContext<unknown>>(_req: Request, ctx: T) => {
        // ZodValidateBodyWithSupabaseContextMiddlewareContext is injected by middleware function - zodValidateBodyWithSupabaseContextMiddlewareFn(...)
        const {
          success,
          data,
          error
        } = (ctx as unknown as ZodValidateBodyWithSupabaseContextMiddlewareContext<z.infer<typeof bodySchema>>).validation;

        if (success) { // pass validation
          const name = data?.name ?? '';
          const age = data?.age ?? 0;
          return new Response(
            JSON.stringify({ success: true, message: `Hello ${name}, you are ${age} years old` }),
            {
              headers: {
                'Content-Type': 'application/json',
              }
            }
          );
        } else {
          const msg = error?.issues.map(issue => issue.message).join(', ') ?? `unknown error`;
          return new Response(
            JSON.stringify({ success: false, message: `error: ${msg}` }),
            {
              headers: {
                'Content-Type': 'application/json',
              }
            }
          );
        }
      }
    }),
  )
}

Typical usage

// NOTE: using old `Deno.serve(...)`
import { applyMiddleware } from "@tmjeee/deno-middleware";
import { corsMiddlewareFn } from "@tmjeee/deno-middleware";
import { httpMethodMiddlewareFn } from "@tmjeee/deno-middleware";
import z from "npm:zod";
import {
  ZodValidateBodyMiddlewareContext,
  zodValidateBodyMiddlewareFn,
} from "@tmjeee/deno-middleware";

interface Body {
  name: string;
  age: number;
}

const BodyType = z.object({
  name: z.string(),
  age: z.number(),
});

Deno.serve(
  {
    port: 3000,
    hostname: "0.0.0.0",
    onListen({ port, hostname }) {
      console.log(`Server is running on http://${hostname}:${port}`);
    },
  },
  applyMiddleware({
    middlewares: [
      corsMiddlewareFn(),
      httpMethodMiddlewareFn("POST"),
      zodValidateBodyMiddlewareFn(BodyType),
    ],
    handler: (_req: Request, ctx: unknown) => {
      // `ZodValidateBodyMiddlewareContext` is injected by middleware function - zodValidateBodyMiddlewareFn(...)
      const {
        body,
        error: _error, // validation error (z.ZodError)
        result: _result, // true or false depending on validation success
      } = (ctx as ZodValidateBodyMiddlewareContext<Body>).validation;

      const name = body.name;
      const age = body.age;

      return new Response(
        JSON.stringify({ success: true, message: `Hello, ${name}! You are ${age} years old.` }),
        {
          headers: {
            "Content-Type": "application/json",
          },
        },
      );
    },
  }),
);

// NOTE: using  new `export default { fetch: withSupabase(...) } `
import { SupabaseContext, withSupabase } from "@supabase/server";
import { applyMiddlewareWithSupabaseContext } from "@tmjeee/deno-middleware/v2";
import { httpMethodWithSupabaseContextMiddlewareFn } from "@tmjeee/deno-middleware/v2";
import {
  ZodValidateBodyWithSupabaseContextMiddlewareContext,
  zodValidateBodyWithSupabaseContextMiddlewareFn,
} from "@tmjeee/deno-middleware/v2";
import z from "zod";

const bodySchema = z.object({
  name: z.string(),
  age: z.number(),
});

export default {
  fetch: withSupabase(
    {
      auth: ["user"],
    },
    applyMiddlewareWithSupabaseContext({
      middlewares: [
        httpMethodWithSupabaseContextMiddlewareFn("POST"),
        zodValidateBodyWithSupabaseContextMiddlewareFn(bodySchema),
        // zodValidationProcessingWithSupabaseContextMiddlewareFn(bodySchema),
      ],
      // deno-lint-ignore require-await
      handler: async <T extends SupabaseContext<unknown>>(_req: Request, ctx: T) => {
        // ZodValidateBodyWithSupabaseContextMiddlewareContext is injected by middleware function - zodValidateBodyWithSupabaseContextMiddlewareFn(...)
        const {
          success,
          data,
          error,
        } = (ctx as unknown as ZodValidateBodyWithSupabaseContextMiddlewareContext<
          z.infer<typeof bodySchema>
        >).validation;

        if (success) { // pass validation
          const name = data?.name ?? "";
          const age = data?.age ?? 0;
          return new Response(
            JSON.stringify({ success: true, message: `Hello ${name}, you are ${age} years old` }),
            {
              headers: {
                "Content-Type": "application/json",
              },
            },
          );
        } else {
          const msg = error?.issues.map((issue) => issue.message).join(", ") ?? `unknown error`;
          return new Response(
            JSON.stringify({ success: false, message: `error: ${msg}` }),
            {
              headers: {
                "Content-Type": "application/json",
              },
            },
          );
        }
      },
    }),
  ),
};

Using Zod for body validation

If you prefer Zod over TypeBox for schema validation, use the Zod-specific middleware:

import { applyMiddleware } from "@tmjeee/deno-middleware";
import { corsMiddlewareFn } from "@tmjeee/deno-middleware";
import { httpMethodMiddlewareFn } from "@tmjeee/deno-middleware";
import { z } from "zod";
import {
  ZodValidateBodyMiddlewareContext,
  zodValidateBodyMiddlewareFn,
} from "@tmjeee/deno-middleware";

const BodySchema = z.object({
  name: z.string().min(1),
  age: z.number().min(0),
});

type Body = z.infer<typeof BodySchema>;

Deno.serve(
  {
    port: 3000,
    hostname: "0.0.0.0",
    onListen({ port, hostname }) {
      console.log(`Server is running on http://${hostname}:${port}`);
    },
  },
  applyMiddleware({
    middlewares: [
      corsMiddlewareFn(),
      httpMethodMiddlewareFn("POST"),
      zodValidateBodyMiddlewareFn<Body>(BodySchema),
    ],
    handler: (_req: Request, ctx: unknown) => {
      const { validation } = ctx as ZodValidateBodyMiddlewareContext<Body>;

      if (!validation.success) {
        return new Response(
          JSON.stringify({ success: false, errors: validation.error.issues }),
          { status: 400, headers: { "Content-Type": "application/json" } },
        );
      }

      const { name, age } = validation.data;

      return new Response(
        JSON.stringify({ success: true, message: `Hello, ${name}! You are ${age} years old.` }),
        {
          headers: {
            "Content-Type": "application/json",
          },
        },
      );
    },
  }),
);

Tip

The context uses a discriminated union with validation.success. When true, you get validation.data (typed). When false, you get validation.error (a ZodError) and validation.input (the raw body).

Writing a MiddlewareFn

A MiddlewareFn receives the request, a shared context object, and a next function to pass control to the next middleware or handler. Return a Response early to short-circuit the chain.

import { MiddlewareFn } from "@tmjeee/deno-middleware";

// A simple logging middleware
const loggingMiddlewareFn: () => MiddlewareFn = () => async (req, ctx, next) => {
  const start = Date.now();
  console.log(`--> ${req.method} ${new URL(req.url).pathname}`);

  const resp = await next(req, ctx);

  console.log(`<-- ${resp.status} (${Date.now() - start}ms)`);
  return resp;
};

// A middleware that adds data to the context
interface AuthContext {
  auth: { userId: string };
}

const authMiddlewareFn: () => MiddlewareFn = () => (req, ctx, next) => {
  const token = req.headers.get("Authorization");
  if (!token) {
    return new Response(
      JSON.stringify({ error: "Unauthorized" }),
      { status: 401, headers: { "Content-Type": "application/json" } },
    );
  }

  // Add auth info to context for downstream middlewares/handler
  (ctx as AuthContext).auth = { userId: "user-123" };
  return next(req, ctx);
};

Writing a HandlerFn

A HandlerFn is the final function in the middleware chain. It receives the request and the shared context, and must return a Response.

import { HandlerFn } from "@tmjeee/deno-middleware";

// Simple handler
const handler: HandlerFn = (_req, _ctx) => {
  return new Response(
    JSON.stringify({ success: true, message: "Hello, World!" }),
    { headers: { "Content-Type": "application/json" } },
  );
};

// Handler that reads from context (e.g. validated body)
interface MyContext {
  validation: { body: { name: string; age: number } };
}

const handlerWithContext: HandlerFn = (_req, ctx) => {
  const { name, age } = (ctx as MyContext).validation.body;
  return new Response(
    JSON.stringify({ success: true, greeting: `Hello ${name}, age ${age}` }),
    { headers: { "Content-Type": "application/json" } },
  );
};

Built-in MiddlewareFns

Old

Middleware Description Usage
corsMiddlewareFn() Handles CORS preflight requests and adds CORS headers corsMiddlewareFn()
httpMethodMiddlewareFn(method) Validates the HTTP method (GET or POST), returns 405 if not allowed httpMethodMiddlewareFn("POST")
validateBodyMiddlewareFn(schema) Validates JSON body using a TypeBox schema validateBodyMiddlewareFn(Type.Object({ name: Type.String() }))
zodValidateBodyMiddlewareFn(schema) Validates JSON body using a Zod schema zodValidateBodyMiddlewareFn<Body>(z.object({ name: z.string() }))

V2

Middleware Description Usage
applyMiddlewareWithSupabaseContext(...) Composes middlewares that integrate with Supabase's SupabaseContext applyMiddlewareWithSupabaseContext({ middlewares: [...], handler })
httpMethodWithSupabaseContextMiddlewareFn(method) Validates the HTTP method (GET or POST), returns 405 if not allowed httpMethodWithSupabaseContextMiddlewareFn("POST")
zodValidateBodyWithSupabaseContextMiddlewareFn(schema) Validates JSON body using a Zod schema (continues chain on failure) zodValidateBodyWithSupabaseContextMiddlewareFn<Body>(schema)
zodValidationProcessingWithSupabaseContextMiddlewareFn(schema) Validates JSON body using a Zod schema (short-circuits with 400 on failure) zodValidationProcessingWithSupabaseContextMiddlewareFn(schema)

Utilities

typedRpcSingle

Performs a type-safe Supabase RPC call and expects a single result or null.

  • Returns the row directly if one row is found.
  • Returns null if no rows are found (does not throw).
  • Throws an error if more than one row is returned.

This is the recommended helper when your RPC function may return zero or one result (uses .maybeSingle() internally).

// Inside a v2 handler / middleware
const profile = await typedRpcSingle(ctx, "get_user_profile", {
  user_id: ctx.user?.id,
});
typedRpcMany

Performs a type-safe Supabase RPC call and returns an array of results (can be empty).

Use this when your RPC function can return zero or more rows.

// Inside a v2 handler / middleware
const posts = await typedRpcMany(ctx, "get_user_posts", {
  user_id: ctx.user?.id,
});

See full details in src/v2/supabase-utils.ts.

API Documentation

Full API documentation is available on JSR.

Development

Prerequisites

Setup

After cloning the repository, install Git hooks:

./scripts/setup-hooks.sh

This sets up pre-commit hooks to automatically check formatting, linting, types, and tests before each commit. See docs/git-hooks.md for more details.

Commands

# Run tests
deno task test

# Run tests in watch mode
deno task test:watch

# Type check
deno task check

# Format code
deno task fmt

# Check formatting
deno task fmt:check

# Lint code
deno task lint

# Generate coverage
deno task coverage

Publishing to JSR

  1. Ensure you're logged in to JSR:

    deno publish --dry-run
  2. Update the version in deno.json

  3. Publish:

    deno publish

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT License - see LICENSE file for details

Links

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

Contributors