Skip to content

Instantly share code, notes, and snippets.

@gmr458
Created January 29, 2026 04:14
Show Gist options
  • Select an option

  • Save gmr458/ed317547a67504bb5e8efb57bfad152f to your computer and use it in GitHub Desktop.

Select an option

Save gmr458/ed317547a67504bb5e8efb57bfad152f to your computer and use it in GitHub Desktop.
Hono + TypeScript

Here's how I get the authenticated user with type safety using Hono and BetterAuth.

Here's the middleware. Using c.set(), I save the user and session in the current request:

import { createMiddleware } from "hono/factory";
import { auth } from "#app/lib/auth";

export const loggedInRequired = createMiddleware<{
    Variables: {
        user: typeof auth.$Infer.Session.user;
        session: typeof auth.$Infer.Session.session;
    };
}>(async (c, next) => {
    const session = await auth.api.getSession({ headers: c.req.raw.headers });
    if (!session) {
        return c.json(
            { message: "Authentication required. Please sign in." },
            401,
        );
    }

    c.set("user", session.user);
    c.set("session", session.session);
    return next();
});

I use the middleware like this:

import { loggedInRequired } from "#app/middlewares";
import { Hono } from "hono";

const router = new Hono();

type Task = {
    id: string;
    userId: string;
    title: string;
    done: boolean;
};

let tasks: Task[] = [];

router.get("/get-todos", loggedInRequired, async (c) => {
    const user = c.get("user");
    const userTasks = tasks.filter((task) => task.userId === user.id);
    return c.json(userTasks, 200);
});

export { router };

Here's the good part: the string that c.get() accepts as an argument is typed, it can only be "user" or "session". If I pass any other string, I get a compilation error.

Also, user and session are guaranteed not to be null or undefined, and they're correctly typed. If I remove the loggedInRequired middleware, I get a compilation error on the line where I call c.get("user"). This is much better than extending the Request type globally with a field that may or may not be defined.

I'll also argue in favor of c.req.valid("json"). I use Zod for validation, look at this example route:

router.post(
    "/add-todo",
    loggedInRequired,
    validateReqBody({
        schema: zCreateTask,
        message: "Invalid task data provided",
        statusCode: 400,
    }),
    async (c) => {
        const user = c.get("user");
        const body = c.req.valid("json");

        const newTask: Task = {
            id: crypto.randomUUID(),
            userId: user.id,
            title: body.title,
            done: false,
        };

        tasks.push(newTask);
        return c.json(newTask, 201);
    },
);

validateReqBody is a utility function in my codebase that works like middleware. It accepts an object with a Zod schema, an error message, and an HTTP status code to return if the request body is invalid.

The value returned by c.req.valid("json") is correctly typed based on the Zod schema. This is quite clean. I also have similar functions called validateRouteParams and parsePaginationQueryParams, these use Zod and Hono's validator.

Also, Hono's validator allows me to validate JSON, form data, query parameters, path parameters, headers, and cookies.

I honestly don't see myself using Express or Fastify over Hono if I have the choice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment