Revalidation for data using next.js 15 route handlers

I've developed an application using Next.js 15, utilizing API routes for the backend. However, I'm encountering an issue where, after posting data, the new information only appears upon manually refreshing the page. Despite looking at official documentation, watching tutorials, and reading articles, I haven't found a solution. I'm beginning to think that to achieve active refresh upon table updates, I might need to use Server Actions instead of API route handlers, since whereever I looked at for revalidation, everyone was using server components. // DataTable.tsx
useEffect(() => {
const fetchClients = async () => {
const response = await fetch("/api/clients", { next: { tags: ['clients'] } });
const data = await response.json();
setClientCells(data);
};
fetchClients();
}, []);
useEffect(() => {
const fetchClients = async () => {
const response = await fetch("/api/clients", { next: { tags: ['clients'] } });
const data = await response.json();
setClientCells(data);
};
fetchClients();
}, []);
// Form.tsx
const submitHandler = async (data: ClientFormData) => {
const response = await fetch("/api/clients", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
revalidateTag("clients")
reset();
setFiles([]);
};
const onSubmit = handleSubmit(submitHandler);

return(
<form
onSubmit={onSubmit}
className="px-10 space-y-5 my-4">
.....
</form>
)
const submitHandler = async (data: ClientFormData) => {
const response = await fetch("/api/clients", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
revalidateTag("clients")
reset();
setFiles([]);
};
const onSubmit = handleSubmit(submitHandler);

return(
<form
onSubmit={onSubmit}
className="px-10 space-y-5 my-4">
.....
</form>
)
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const parsedBody = clientSchema.safeParse(body);
const client = await prisma.clients.create({
data: parsedBody.data,
});
return NextResponse.json(
{ message: "Client created successfully", client },
{ status: 201 }
);
} catch (error) {
return NextResponse.json(
{ error: "An unexpected error occurred while creating the client." },
{ status: 500 }
);
}
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const parsedBody = clientSchema.safeParse(body);
const client = await prisma.clients.create({
data: parsedBody.data,
});
return NextResponse.json(
{ message: "Client created successfully", client },
{ status: 201 }
);
} catch (error) {
return NextResponse.json(
{ error: "An unexpected error occurred while creating the client." },
{ status: 500 }
);
}
}
4 Replies
Hamza Ali Turi
Hamza Ali TuriOP5w ago
Upon someone's suggestion, I used router.refresh() but that didnt work as well. The only thing that seems to work is window.location.reload() which ends up doing a hard refresh.
Friedrich
Friedrich5w ago
Have you tried to keep revalidateTag on the server side (on the API route) ? Since it is a cache related function, it should be called on the server side
Hamza Ali Turi
Hamza Ali TuriOP5w ago
It only works for server components from what I've read. I was able to come up with a solution, it looks like the way I was fetching the data was wrong: here is the updated code:
import { ClientInterface } from "@/app/_lib/types";
import ClientsDataTable from "./_components/ClientsDataTable";

async function getClients(): Promise<ClientInterface[]> {
const res = await fetch("http://localhost:3000/api/clients");
if (!res.ok) throw new Error("Failed to fetch clients");
return res.json();
}

export default async function ClientsPage() {
const clients = await getClients();
return <ClientsDataTable initialClients={clients} />;
}
import { ClientInterface } from "@/app/_lib/types";
import ClientsDataTable from "./_components/ClientsDataTable";

async function getClients(): Promise<ClientInterface[]> {
const res = await fetch("http://localhost:3000/api/clients");
if (!res.ok) throw new Error("Failed to fetch clients");
return res.json();
}

export default async function ClientsPage() {
const clients = await getClients();
return <ClientsDataTable initialClients={clients} />;
}
const ClientsDataTable = ({ initialClients }: ClientsDataTableProps) => {
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;

const filteredClients = useMemo(() =>
initialClients.filter(client =>
client.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
client.website.toLowerCase().includes(searchQuery.toLowerCase())
), [initialClients, searchQuery]);

const totalPages = Math.ceil(filteredClients.length / itemsPerPage);
const paginatedClients = useMemo(() =>
filteredClients.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage),
[filteredClients, currentPage, itemsPerPage]
);
....
const ClientsDataTable = ({ initialClients }: ClientsDataTableProps) => {
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 5;

const filteredClients = useMemo(() =>
initialClients.filter(client =>
client.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
client.website.toLowerCase().includes(searchQuery.toLowerCase())
), [initialClients, searchQuery]);

const totalPages = Math.ceil(filteredClients.length / itemsPerPage);
const paginatedClients = useMemo(() =>
filteredClients.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage),
[filteredClients, currentPage, itemsPerPage]
);
....
Friedrich
Friedrich5w ago
They work everywhere on the server side, especially with server actions and API routes. Often we use revalidate (revalidateTag or revalidatePath) after a mutation. Mutations generally don't happen in server components. Because they are mostly used to fetch data when needed, not to mutate data. I have used revalidateTag in both server actions and API routes and they both work Anyways, glad to see you have already found a way to solve the problem 👍

Did you find this page helpful?