Can i use server actions for user registration, if so, how do i handle errors
Unanswered
Silver carp posted this in #help-forum
Silver carpOP
when it comes to status codes. for example when i create my signUp action in /auth/actions and hook it up isng useActionState with valid schema and form state the network /register route alwasy returns status 200, even when an error occurs.
// actions/auth/signUp.ts
"use server";
import bcrypt from "bcryptjs";
import { prisma } from "~/lib/prisma";
import { signUpSchema, FormState } from "~/lib/definitions";
export const signUp = async (
formState: FormState,
formData: FormData,
): Promise<FormState> => {
const validatedFields = signUpSchema.safeParse({
username: formData.get("username"),
password: formData.get("password"),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
const { username, password } = validatedFields.data;
if (!username || !password) {
return {
errors: {
username: ["The username field is required"],
password: ["The password field is required"],
},
};
}
const existingUser = await prisma.user.findUnique({
where: { username },
});
if (existingUser) {
return {
errors: {
username: ["This username is already taken"],
},
message: "Try a different username",
};
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
username,
password: hashedPassword,
},
});
if (!user) {
return {
errors: {
username: ["An error occurred while creating the user"],
},
};
}
return {
errors: {},
message: "User created successfully",
};
};
13 Replies
Silver carpOP
// page.ts
"use client";
import { useActionState } from "react";
import { signUp } from "../actions/auth/signUp";
export default function SignUpForm() {
const [state, action, pending] = useActionState(signUp, undefined);
return (
<main className="m-10">
<h1>Sign Up Form</h1>
<div className="flex items-center justify-start rounded-md p-10">
<form action={action} name="signUp">
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<label htmlFor="username">Username</label>
<input
className="border-2 border-black"
type="username"
id="username"
name="username"
required
/>
</div>
{state?.errors?.username && <p>{state.errors.username}</p>}
<div className="flex gap-2">
<label htmlFor="password">Password</label>
<input
className="border-2 border-black"
type="password"
id="password"
name="password"
required
/>
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button
disabled={pending}
className="rounded-md border-2 border-black p-2"
type="submit"
>
Sign Up
</button>
</div>
</form>
</div>
</main>
);
}
// lib/definitions.ts
import { z } from "zod";
export const signUpSchema = z.object({
username: z
.string()
.min(3, { message: "The username must be atleast 3 characters" })
.max(20, { message: "The username cannot be longer than 20 characters" })
.trim(),
password: z
.string()
.min(8, { message: "The password must be atleast 8 characters" })
.regex(/[a-zA-Z0-9]/, {
message: "The password must contain atleast one letter and one number",
})
.trim(),
});
export type FormState =
| {
errors?: {
username?: string[];
password?: string[];
};
message?: string;
}
| undefined;
this is after submission
Silver carpOP
no user is created which means its working but why does it always return a 200 response. ive tried returnign a NextResponse.json() object within the same params and altering the form state and removing it, but then i get a front end error as with useActionState you need to have the state arg.
Sun bear
Because your returning a json with field errors
When using server actions you're server actions you're calling functions not api routes, so you can't return status code, or let me say complete api response
If you're using a api endpoint then you can with something like this
// Get all recipes
export async function GET(): Promise<NextResponse> {
try {
const client = await clientPromise;
const db = client.db();
const recipes = await db.collection("recipes").find({}).toArray();
return NextResponse.json(recipes, { status: 200, statusText: "OK" });
} catch (error) {
console.log("RECIPES_GET: ", error);
return NextResponse.json(
{ error: "Failed to fetch recipes" },
{ status: 500, statusText: "INTERNAL SERVER ERROR" }
);
}
}
With Server Actions you’re basically calling an API, since it’s making a POST request under the hood. But still, you can’t return status codes from the traditional Response object.
What you can do is returning an object containing information, something like this:
You can improve this by typing your Server Action return type, maybe with discriminate unions.
This way you’re enforcing the implementation of these response objects. Also, server actions are the used to handle mutations which means the user is expecting some kind of response back from them, you can (or shouldn’t) redirect or break the app for the the user from inside, instead handle the error and communicate it back to the user.
What you can do is returning an object containing information, something like this:
//success cases
const response = { success: true, data: {}, status: 200, message: “message”}
//error cases
const response = { success: false, status: 400 || 500, message: “error message”, errors:{} }
You can improve this by typing your Server Action return type, maybe with discriminate unions.
This way you’re enforcing the implementation of these response objects. Also, server actions are the used to handle mutations which means the user is expecting some kind of response back from them, you can (or shouldn’t) redirect or break the app for the the user from inside, instead handle the error and communicate it back to the user.
@Silver carp when it comes to status codes. for example when i create my signUp action in /auth/actions and hook it up isng useActionState with valid schema and form state the network /register route alwasy returns status 200, even when an error occurs.
// actions/auth/signUp.ts
"use server";
import bcrypt from "bcryptjs";
import { prisma } from "~/lib/prisma";
import { signUpSchema, FormState } from "~/lib/definitions";
export const signUp = async (
formState: FormState,
formData: FormData,
): Promise<FormState> => {
const validatedFields = signUpSchema.safeParse({
username: formData.get("username"),
password: formData.get("password"),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
const { username, password } = validatedFields.data;
if (!username || !password) {
return {
errors: {
username: ["The username field is required"],
password: ["The password field is required"],
},
};
}
const existingUser = await prisma.user.findUnique({
where: { username },
});
if (existingUser) {
return {
errors: {
username: ["This username is already taken"],
},
message: "Try a different username",
};
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await prisma.user.create({
data: {
username,
password: hashedPassword,
},
});
if (!user) {
return {
errors: {
username: ["An error occurred while creating the user"],
},
};
}
return {
errors: {},
message: "User created successfully",
};
};
When you use useActionState and pass a server action to it, not this server action is bound to a different contract, now it NEEDS to take “state” as the first argument, which means you need to provide two parameters in the server action.
export async function myAction (initialState, FormData){}
Silver carpOP
Thank you 🙂