H
Hono2w ago
Kalpak

zValidator c.req.valid("form") type error

Hey everyone, I am trying to use the zValidator middleware which I have setup in an auth.validator.ts file which looks like:
const signUpSchema = z.object({
email: z.string().email("Invalid email address"),
password: z
.string()
.regex(
passwordRegex,
"Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, and one number"
),
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
// TODO: Add more specific validation for dob (date format) and gender (enum) if needed
dob: z
.string()
.regex(
dateRegex,
"Date must be in YYYY-MM-DD format with valid month and day"
),
gender: z.enum(["male", "female", "other"]),
});

// zValidator middleware instance for the sign-up form
const signUpValidator = zValidator("form", signUpSchema);
const signUpSchema = z.object({
email: z.string().email("Invalid email address"),
password: z
.string()
.regex(
passwordRegex,
"Password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, and one number"
),
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
// TODO: Add more specific validation for dob (date format) and gender (enum) if needed
dob: z
.string()
.regex(
dateRegex,
"Date must be in YYYY-MM-DD format with valid month and day"
),
gender: z.enum(["male", "female", "other"]),
});

// zValidator middleware instance for the sign-up form
const signUpValidator = zValidator("form", signUpSchema);
I have my route setup like:
app.post(
"/signup",
authValidator.signUpValidator,
signUpController.signUpWithEmail
);
app.post(
"/signup",
authValidator.signUpValidator,
signUpController.signUpWithEmail
);
and signUpController like:
async function signUpWithEmail(c: Context) {
// const { email, password, firstName, lastName, dob, gender } =
// await c.req.parseBody();

// const userMetadata = {
// first_name: firstName,
// last_name: lastName,
// dob,
// gender,
// };

const body = c.req.valid('form');
console.log(body);

// const { error } = await authService.signUpWithEmail(
// c,
// email as string,
// password as string,
// userMetadata as UserMetadata
// );

// if (error == null) {
// return c.render(<EmailVerificationCTA email={email as string} />);
// }

c.status(400);
return c.redirect("/auth/error");
}

export default { signUpWithEmail };
async function signUpWithEmail(c: Context) {
// const { email, password, firstName, lastName, dob, gender } =
// await c.req.parseBody();

// const userMetadata = {
// first_name: firstName,
// last_name: lastName,
// dob,
// gender,
// };

const body = c.req.valid('form');
console.log(body);

// const { error } = await authService.signUpWithEmail(
// c,
// email as string,
// password as string,
// userMetadata as UserMetadata
// );

// if (error == null) {
// return c.render(<EmailVerificationCTA email={email as string} />);
// }

c.status(400);
return c.redirect("/auth/error");
}

export default { signUpWithEmail };
When I am trying to use c.req.valid("form") i get the error: Argument of type '"form"' is not assignable to parameter of type 'never'. Im assuming its because the validator middleware is defined in and the route is setup in index.ts. how do I fix this? Do I have to type the Env being passed into the hono router created in index.ts? If so, how?
17 Replies
Kalpak
KalpakOP2w ago
Figured it out by using a factory
ambergristle
ambergristle2w ago
Context takes three generic arguments. To abstract validation like this, you’d need to specify Input This level of abstraction conflicts with how Hono is typed/designed. You can do it, but you’ll need to work around it, as you do here factory.createApp?
Arjix
Arjix2w ago
the docs discourage RoR-like controllers like yours for the exact issues you are facing (even though I fail to see how that is a controller, coming from an ASP.NET background)
Kalpak
KalpakOP2w ago
I used createHandlers:
import { createFactory } from "hono/factory";
import authValidator from "@/routes/auth/validators/auth.validators";
import authService, { UserMetadata } from "@/routes/auth/services/auth.service";
import EmailVerificationCTA from "@/routes/auth/views/EmailVerificationCTA";

const signUpFactory = createFactory();

const signUpWithEmailHandlers = signUpFactory.createHandlers(
authValidator.signUpValidator,
async (c) => {
const { email, password, firstName, lastName, dob, gender } =
c.req.valid("form");

const userMetadata = {
first_name: firstName,
last_name: lastName,
dob,
gender,
};

const { error } = await authService.signUpWithEmail(
c,
email as string,
password as string,
userMetadata as UserMetadata
);

if (error == null) {
return c.render(<EmailVerificationCTA email={email as string} />);
}

c.status(400);
return c.redirect("/auth/error");
}
);

export default { signUpWithEmailHandlers };
import { createFactory } from "hono/factory";
import authValidator from "@/routes/auth/validators/auth.validators";
import authService, { UserMetadata } from "@/routes/auth/services/auth.service";
import EmailVerificationCTA from "@/routes/auth/views/EmailVerificationCTA";

const signUpFactory = createFactory();

const signUpWithEmailHandlers = signUpFactory.createHandlers(
authValidator.signUpValidator,
async (c) => {
const { email, password, firstName, lastName, dob, gender } =
c.req.valid("form");

const userMetadata = {
first_name: firstName,
last_name: lastName,
dob,
gender,
};

const { error } = await authService.signUpWithEmail(
c,
email as string,
password as string,
userMetadata as UserMetadata
);

if (error == null) {
return c.render(<EmailVerificationCTA email={email as string} />);
}

c.status(400);
return c.redirect("/auth/error");
}
);

export default { signUpWithEmailHandlers };
Do you have a recommendation on best practices?
Arjix
Arjix2w ago
Best Practices - Hono
Web framework built on Web Standards for Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Node.js, and others. Fast, but not only fast.
ambergristle
ambergristle2w ago
inline everything until you need to abstract something out
Arjix
Arjix2w ago
sums it up pretty well
Kalpak
KalpakOP2w ago
hmm, I see. It felt a lot cleaner to do it in the RoR style. Beside complex typing are there performance complications ?
Arjix
Arjix2w ago
I think your coding background defines that ngl (not talking about performance lol) my first backend was in ASP.NET, so controllers for me are way too different from the controllers you imagine src/controllers/AuthController.ts
export const AuthController = new Hono().basePath("/auth")
.post("/login", async (ctx) => {
// do stuff
})
.post("/logout", async (ctx) => {
// do stuff
});
export const AuthController = new Hono().basePath("/auth")
.post("/login", async (ctx) => {
// do stuff
})
.post("/logout", async (ctx) => {
// do stuff
});
src/index.ts
const app = new Hono()
.route("/", AuthController)
// ...other controllers

export type APP = typeof app;
const app = new Hono()
.route("/", AuthController)
// ...other controllers

export type APP = typeof app;
is how I'd shape a controller and I design my middleware to be idempotent, so I can call them multiple times across multiple controllers w/o care TL;DR, design your middleware to do the heavy lifting once
export type UserSession = (Omit<SessionObject, 'user'> & { user: UserObject });
export type AuthVariables = { session: UserSession | null };

export const auth = createMiddleware<{ Variables: AuthVariables }>(
async (ctx, next) => {
// HERE
if (ctx.get('session')) return await next();
// ^^ HERE we gain idemptotance

const sessionHandler = container.resolve(tokens.SessionHandler);

const headerToken = ctx.req.header('token');
const cookieToken = getCookie(ctx, 'token');

const token = cookieToken ?? headerToken;

const session = token ? await sessionHandler.touch(token) : null;

if (session) {
setCookie(ctx, 'token', session.token, {
// secure: true,
httpOnly: true,
expires: session.expires
});
}

ctx.set('session', session);
return await next();
}
);
export type UserSession = (Omit<SessionObject, 'user'> & { user: UserObject });
export type AuthVariables = { session: UserSession | null };

export const auth = createMiddleware<{ Variables: AuthVariables }>(
async (ctx, next) => {
// HERE
if (ctx.get('session')) return await next();
// ^^ HERE we gain idemptotance

const sessionHandler = container.resolve(tokens.SessionHandler);

const headerToken = ctx.req.header('token');
const cookieToken = getCookie(ctx, 'token');

const token = cookieToken ?? headerToken;

const session = token ? await sessionHandler.touch(token) : null;

if (session) {
setCookie(ctx, 'token', session.token, {
// secure: true,
httpOnly: true,
expires: session.expires
});
}

ctx.set('session', session);
return await next();
}
);
Kalpak
KalpakOP2w ago
I see, would you refactor any of the logic into different files or would everything (validation, data access, view) for auth live in AuthController.ts?
Arjix
Arjix2w ago
depends if it has to do with db, I have made a repository for it if it has to do with validation, I either inline the zod schema, or place it in a separate file, depends on how re-usable the schema is
Arjix
Arjix2w ago
here is another example of a controller
No description
Arjix
Arjix2w ago
(I didn't chain everything together cause I don't use the rpc)
ambergristle
ambergristle2w ago
new files, or other forms of abstraction, reduce the cognitive load of their context, but increase your app's complexity in other ways understanding when to abstract something is probably a life-long journey but in my experience, abstracting something just to follow a pattern, or some idea of how to code, typically hurts more than it helps it's only when you repeat something a bunch that you start to get an idea of what an abstraction actually needs to accomplish to be useful, in that specific case
Arjix
Arjix2w ago
I think a combination of KISS and DRY are perfect for hono Keep It Simple Silly + Don't Repeat Yourself
ambergristle
ambergristle2w ago
@Kalpak i think you're right to break out things like db and auth, for example. but they're mostly backend code, and trying to make them entirely self-contained (as you might in node) isn't in sync with how cloudflare works your endpoint logic also looks good to me but abstracting with createHandlers seems to be adding more complexity to your implementation than it's worth
Kalpak
KalpakOP2w ago
Got it, appreaciate all the detailed responses and help. Thanks!

Did you find this page helpful?