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.