Middleware functions / utilities for use with Supabase Deno http server.
- 🚀 Simple middleware function
applyMiddleware(...)forDeno.serve(...)for Supabase Edge Functions - 📦 Common middleware out of the box eg. (
corsMiddlewareFn(),httpMethodMiddlewareFn(...)etc.) - 🔒 Easy to add new middleware, handler functions (
MiddlewareFnandHandlerFntype respectively) - ✨ TypeScript support out of the box
- 🧪 Well tested (Hopefully !!!)
- 📚 Fully documented (Hopefully !!!)
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"
}
}For Deno server running in Supabase Edge Functions only. Doesn't make sense to run it in Node.
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.
// 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',
}
}
);
}
}
}),
)
}// 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",
},
},
);
}
},
}),
),
};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).
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);
};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" } },
);
};| 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() })) |
Performs a type-safe Supabase RPC call and expects a single result or null.
- Returns the row directly if one row is found.
- Returns
nullif 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,
});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.
Full API documentation is available on JSR.
- Deno or higher
After cloning the repository, install Git hooks:
./scripts/setup-hooks.shThis sets up pre-commit hooks to automatically check formatting, linting, types, and tests before each commit. See docs/git-hooks.md for more details.
# 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-
Ensure you're logged in to JSR:
deno publish --dry-run
-
Update the version in
deno.json -
Publish:
deno publish
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE file for details
