Skip to main content
🧙‍♂️ refine grants your wishes! Please give us a ⭐️ on GitHub to keep the magic going.
Version: 4.xx.xx

Server-Side Form Validation

Server-side form validation is a technique used to validate form data on the server before processing it. Unlike client-side validation, which is performed in the user's browser using JavaScript, server-side validation occurs on the server-side code, typically in the backend of the application.

Why Server-Side Validation?

Client-side validation offers a responsive user experience by providing immediate feedback without server round trips. However, it should not be considered a substitute for server-side validation due to its vulnerability to bypassing. Server-side form validation is essential for ensuring data integrity, security, and consistency. It acts as an additional layer that complements client-side validation, preventing malicious or incorrect data from being processed. While client-side validation is valuable, it should not be relied upon exclusively, as server-side validation provides a more robust and reliable validation mechanism.

Server-Side Validation with refine

refine supports server-side validation out-of-the-box for all supported UI frameworks (Ant Design, Material UI, Mantine, Chakra UI).

To handle server-side validation errors, you need to return a rejected promise with the following shape from the dataProvider:

import { HttpError } from "@refinedev/core";

const error: HttpError = {
message: "An error occurred while updating the record.",
statusCode: 400,
// the errors field is required for server-side validation.
// when the errors field is set, useForm will automatically display the error messages in the form with the corresponding fields.
errors: {
title: ["Title is required"],
content: {
key: "form.error.content",
message: "Content is required.",
},
tags: true,
},
};

Refer to the HttpError type here.

errors fields can be string or string[] or boolean or { key: string; message: string }

  • string or string[]: If the field is an array, multiple error messages will be displayed. If the field is a string, only one error message will be displayed.
  • boolean: If the field is true, "This field is required." message will be displayed. If the field is false, no error message will be displayed.
  • { key: string; message: string }: If the field is an object, the key field will be used as a translation key. If the key is not found in the translation, the message field will be displayed.

How does it work?

When the dataProvider returns a rejected promise with errors field, useForm hook will assign errors to the respective form fields.

How to disable it?

To disable server-side validation, you have two options:

App.tsx
import { Refine } from "@refinedev/core";

const App: React.FC = () => {
return (
<Refine
// ...
options={{
disableServerSideValidation: true,
}}
>
// ...
</Refine>
);
};
  • disable it for a specific form from the useForm hook.
import { useForm } from "@refinedev/mantine";
OR;
import { useForm } from "@refinedev/react-hook-form";
OR;
import { useForm } from "@refinedev/antd";

useForm({
disableServerSideValidation: true,
});

Examples

In the following examples, we will use this mock dataProvider to demonstrate how to handle server-side validation.

import { HttpError, Refine } from "@refinedev/core";
import dataProvider from "@refinedev/simple-rest";

const App = () => {
return (
// ---
<Refine
// ---
dataProvider={{
...dataProvider("https://api.fake-rest.refine.dev"),
// this is demonstration of how you can handle errors from API
update: async () => {
const error: HttpError = {
message: "An error occurred while updating the record.",
statusCode: 400,
errors: {
title: ["Title is required."],
"category.id": ["Category is required."],
status: ["Status is required."],
content: {
key: "form.error.content",
message: "Content is required.",
},
tags: ["Tags is required."],
},
};

return Promise.reject(error);
},
create: async () => {
// this is demonstration of how you can handle errors from API
const error: HttpError = {
message: "An error occurred while creating the record.",
statusCode: 400,
errors: {
title: ["Title is required."],
"category.id": ["Category is required."],
status: ["Status is required."],
content: {
key: "form.error.content",
message: "Content is required.",
},
tags: ["Tags is required."],
},
};
return Promise.reject(error);
},
}}
// ---
>
// ---
</Refine>
);
};

with Core useForm

You can find more information about the useForm hook here.

Due to the fact that useForm hook is framework agnostic, you need to render the errors returned from the dataProvider manually.

When dataProvider returns rejected promise with errors field, useForm hook will return errors state, which is an object with the following shape:

import { useForm } from "@refinedev/core";

const form = useForm({
// ...
});

// you can access the errors state from the useForm hook
console.log(form.mutationResult.error?.errors);

with React Hook Form

You can find more information about the useForm hook here.

Due to the fact that useForm hook is framework agnostic, you need to render the errors returned from the dataProvider manually.

When dataProvider returns rejected promise with errors field, useForm hook will return errors state, which is an object with the following shape:

import { useForm } from "@refinedev/core";

const form = useForm({
// ...
});

// you can access the errors state from the useForm hook
console.log(form.formState.errors);

with Ant Design

localhost:3000/edit/123
import React from "react";
import { HttpError, IResourceComponentsProps } from "@refinedev/core";
import { Edit, useForm } from "@refinedev/antd";

import { Form, Input } from "antd";

import { IPost, ICategory } from "../../interfaces";

const PostEdit: React.FC<IResourceComponentsProps> = () => {
const { formProps, saveButtonProps } = useForm();

return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item label="Title" name="title">
<Input />
</Form.Item>
<Form.Item label="Content" name="content">
<Input />
</Form.Item>
</Form>
</Edit>
);
};

You can find more information about the useForm hook here.

For this example, we mock data provider to return rejected promise with errors field. You can see full example here

When dataProvider returns rejected promise with errors field, useForm automatically set the form.errors state with the error messages returned from the dataProvider.

You can pass formProps to the <Form> component to display the error messages. <Form> component will automatically display the error messages for the corresponding fields.

Here is an code of how you can display the error messages:

import { useForm } from "@refinedev/antd";
import { Form } from "antd";

const Page = () => {
const { formProps } = useForm();

// ...

return (
// ...
<Form {...formProps}>
<Form.Item label="Title" name="title">
<Input />
</Form.Item>
</Form>
);
};

with Mantine

localhost:3000/edit/123
import { Edit as MantineEdit, useForm } from "@refinedev/mantine";
import {
Input as MantineInput,
TextInput as MantineTextInput,
Textarea as MantineTextarea,
} from "@mantine/core";

interface IPost {
title: string;
content: string;
}

const PostEdit: React.FC = () => {
const { saveButtonProps, getInputProps, errors } = useForm<
IPost,
HttpError,
IPost
>({
initialValues: {
title: "",
content: "",
},
});

return (
<MantineEdit saveButtonProps={saveButtonProps}>
<form>
<MantineTextInput
mt={8}
label="Title"
placeholder="Title"
{...getInputProps("title")}
/>

<MantineTextarea
label="Content"
placeholder="Content"
minRows={4}
maxRows={4}
{...getInputProps("content")}
/>
</form>
</MantineEdit>
);
};

You can find more information about the useForm hook here.

For this example, we mock data provider to return rejected promise with errors field. You can see full example here

When dataProvider returns rejected promise with errors field, useForm automatically set the form.errors state with the error messages returned from the dataProvider.

You can pass getInputProps(<field-name>) to the input component to display the error messages.

Here is an code of how you can display the error messages:

import { useForm } from "@refinedev/mantine";
import { TextInput } from "@mantine/core";

const Page = () => {
const { errors, getInputProps } = useForm();

// ...

return (
// ...
<TextInput
id="title"
label="Title"
placeholder="Title"
{...getInputProps("title")}
/>
);
};

with Material UI

localhost:3000/edit/123
import { Edit } from "@refinedev/mui";
import Box from "@mui/material/Box";
import TextField from "@mui/material/TextField";
import { useForm } from "@refinedev/react-hook-form";

interface IPost {
title: string;
content: string;
}

const PostEdit: React.FC = () => {
const {
saveButtonProps,
refineCore: { queryResult },
register,
control,
formState: { errors },
} = useForm<IPost, HttpError, Nullable<IPost>>();

return (
<Edit saveButtonProps={saveButtonProps}>
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField
id="title"
{...register("title")}
error={!!errors.title}
helperText={errors.title?.message}
margin="normal"
fullWidth
label="Title"
name="title"
autoFocus
/>

<TextField
id="content"
{...register("content")}
error={!!errors.content}
helperText={errors.content?.message}
margin="normal"
label="Content"
multiline
rows={4}
/>
</Box>
</Edit>
);
};

You can find more information about the useForm hook here.

For this example, we mock data provider to return rejected promise with errors field. You can see full example here

When dataProvider returns rejected promise with errors field, useForm automatically set the formState.errors state with the error messages returned from the dataProvider. You can pass formState.errors.status.message to the input component to display the error messages.

Here is an code of how you can display the error messages:

import TextField from "@mui/material/TextField";
import { useForm } from "@refinedev/react-hook-form";

const Page = () => {
const {
register,
formState: { errors },
} = useForm();

// ...

return (
// ...
<TextField
{...register("title")}
error={!!errors.status}
helperText={errors.status?.message}
/>
);
};

with Chakra UI

localhost:3000/edit/123
import { HttpError } from "@refinedev/core";
import { useForm } from "@refinedev/react-hook-form";
import { Edit } from "@refinedev/chakra-ui";
import {
FormControl,
FormErrorMessage,
FormLabel,
Input,
Textarea,
} from "@chakra-ui/react";

interface IPost {
title: string;
content: string;
}

const PostEdit: React.FC = () => {
const {
refineCore: { formLoading, queryResult },
saveButtonProps,
register,
formState: { errors },
setValue,
} = useForm<IPost, HttpError, IPost>();

return (
<Edit isLoading={formLoading} saveButtonProps={saveButtonProps}>
<FormControl mb="3" isInvalid={!!errors?.title}>
<FormLabel>Title</FormLabel>
<Input id="title" type="text" {...register("title")} />
<FormErrorMessage>
{`${errors.title?.message}`}
</FormErrorMessage>
</FormControl>

<FormControl mb="3" isInvalid={!!errors?.content}>
<FormLabel>Content</FormLabel>
<Textarea id="content" {...register("content")} />
<FormErrorMessage>
{`${errors.content?.message}`}
</FormErrorMessage>
</FormControl>
</Edit>
);
};

You can find more information about the useForm hook here.

For this example, we mock data provider to return rejected promise with errors field. You can see full example here

When dataProvider returns rejected promise with errors field, useForm automatically set the formState.errors state with the error messages returned from the dataProvider. You can pass formState.errors.status.message to the input component to display the error messages.

Here is an code of how you can display the error messages:

import {
FormControl,
FormErrorMessage,
FormLabel,
Input,
} from "@chakra-ui/react";
import { useForm } from "@refinedev/react-hook-form";

const Page = () => {
const {
register,
formState: { errors },
} = useForm();

// ...

return (
// ...
<FormControl isInvalid={!!errors?.title}>
<FormLabel>Title</FormLabel>
<Input id="title" type="text" {...register("title")} />
<FormErrorMessage>{`${errors.title?.message}`}</FormErrorMessage>
</FormControl>
);
};