Uploader un fichier avec Next.js 15

Découvrez comment implémenter facilement l'upload de fichiers dans Next.js 15 avec React-Hook-Form et React-Query grâce à un hook réutilisable et prêt à l'emploi.
Next.js est un framework vraiment pratique et qui a changé beaucoup de choses pour les développeurs React. Désormais il est simple de concevoir des sites entiers avec du render front et back.
Mais souvent, dans nos projets nous avons besoin de gérer l'upload, l'envoie de fichier est une des fonctionnalités d'interaction les plus communes.
Comment uploader un fichier avec Next.js ?
Pourtant, même si c'est un besoin qui revient souvent, il n'y a aucun outil spécifique qui est mis à disposition pour faire cela de la manière la plus simple.
Je vous propose de vous montrer un exemple de comment je fais un upload avec Next.js 15 en utilisant React-Hook-Form et React-Query.
Je vais vous fournir un hook que j'utilise et que vous pourrez copier coller facilement dans vos projets.
Installation des dépendances
Pour faire ce petit projet, on va installer React-Hook-Form et React-Query.
Pour info j'utilise node 22 ou bun.
npm add react-hook-form
npm add @tanstack/react-query
# ou
bun add react-hook-form
bun add @tanstack/react-query
Arborescence
Nous allons créer les fichiers de manière à obtenir cet arborescence lorsque nous aurons créé tous nos scripts:
project-root/
├── src/
│ ├── app/
│ │ ├── page.tsx # Page d'accueil avec le formulaire
│ │ └── api/
│ │ └── upload/
│ │ └── route.ts # Route API pour l'upload
│ ├── components/
│ │ ├── wrapper.tsx # Wrapper pour React-Query
│ │ └── uploadForm.tsx # Formulaire d'upload
│ └── hooks/
│ └── useUpload.tsx # Hook pour gérer l'upload
├── uploads/ # Dossier où sont stockés les fichiers uploadés
│ └── [fichiers uploadés]
├── node_modules/
├── package.json
└── package-lock.json # ou bun.lockb si vous utilisez Bun
Gestion de l'upload en frontend
Tout d'abord mettons en place nos vues et tout ce que nous allons utiliser pour mettre en place l'upload sur nos vues React.
1 - Activation de React-Query
Pour que React-Query fonctionne, il faut wrapper nos éléments dans un QueryProvider, c'est ce que j'ai fait rapidement dans un composant src/components/wrapper.tsx:
// src/components/wrapper.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
Je l'ai importé ensuite dans ma page Home, src/app/page.tsx, vous pouvez très bien l'importer dans votre layout.tsx:
// src/app/page.tsx
import UploadForm from "@/components/uploadForm";
import { Wrapper } from "@/components/wrapper";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<Wrapper>
<UploadForm />
</Wrapper>
</div>
);
}
2 - Création du hook
Je crée un hook qui va me permettre d'appeler le backend facilement et de gérer les état:
- est en train d'uploader ⏳
- upload ok, le fichier est envoyé ✅
- upload ko, l'upload a échoué ❌
Je crée un hook useUpload dans un fichier src/hooks/useUpload.tsx:
// src/hooks/useUpload.tsx
"use client";
import { useMutation } from "@tanstack/react-query";
export const useUpload = () => {
const uploadFn = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
return response;
};
return useMutation({
mutationFn: uploadFn,
});
};
3 - Création du formulaire
Je me crée un formulaire d'upload dans un composant src/components/uploadForm.tsx.
Je fais en sorte de créer des actions en cas de succès et d'échec:
- Si le fichier est bien uploadé, j'affiche un alert message Uploaded
- Si l'upload a échoué, j'affiche un message Error
Vous pourrez facilement personnaliser ces actions avec un toaster ou une redirection.
// src/components/uploadForm.tsx
"use client";
import { useForm } from "react-hook-form";
import { useUpload } from "@/hooks/useUpload";
type UploadData = {
file: FileList;
};
export default function UploadForm() {
const { register, handleSubmit } = useForm<UploadData>();
const { mutate: upload, isPending } = useUpload();
const onSubmit = (data: UploadData) => {
if (!data.file?.[0]) return;
upload(data.file[0], {
onSuccess: (response) => {
if (response.status === 200) {
alert("Uploaded");
} else {
alert("Error");
}
},
onError: () => {
alert("Error");
},
});
};
return isPending ? (
<div>Uploading...</div>
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col gap-2 mb-4">
<label htmlFor="file">Picture</label>
<input
{...register("file")}
type="file"
accept="image/*"
className="border border-gray-300 p-2 bg-gray-100 rounded-md cursor-pointer"
/>
</div>
<button type="submit" className="bg-blue-500 text-white p-2 rounded-md">
Upload
</button>
</form>
);
}
Gestion de l'upload en backend
Il ne nous reste plus qu'à créer notre route et à traiter le fichier après l'upload.
Dans la plupart des framework, lorsqu'un fichier est uploadé, il est envoyé dans un dossier temporaire de votre système, c'est à vous de l'écrire ailleurs.
Ici nous allons procéder à une écriture dans un dossier uploads/ à la racine du projet.
Je crée un endpoint api/upload en créant un fichier src/app/api/upload/route.ts:
// src/app/api/upload/route.ts
import fs from "fs";
import path from "path";
const DIR_PATH = path.resolve(`./uploads`);
if (!fs.existsSync(DIR_PATH)) {
fs.mkdirSync(DIR_PATH, { recursive: true });
}
export async function POST(req: Request) {
try {
const formData = await req.formData();
const file = formData.get("file") as File;
if (!file) {
return new Response(
JSON.stringify({ error: "Aucun fichier n'a été fourni" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
if (!(file instanceof Blob)) {
return new Response(
JSON.stringify({ error: "Format de fichier invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}
// Récupération des informations du fichier
const filename = file.name;
const fileType = file.type;
const fileSize = file.size;
// Conversion du fichier en ArrayBuffer, puis en Buffer pour la manipulation
const fileArrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(fileArrayBuffer);
// Enregistrement
const filePath = path.resolve(DIR_PATH, filename);
await fs.writeFileSync(filePath, buffer);
return new Response(
JSON.stringify({
success: true,
filename,
fileType,
fileSize,
destination: filePath,
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch (error) {
console.error("Erreur lors du traitement du fichier:", error);
return new Response(
JSON.stringify({
error: "Erreur lors du traitement du fichier",
details: error instanceof Error ? error.message : String(error),
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
}
Tests
Lorsque je me rends sur mon url de homepage, j'ai désormais mon composant d'upload qui s'affiche:
J'upload un fichier et j'ai bien mon message d'alert.
Vérification dans mes fichiers si j'ai bien tout reçu:
Le fichier a bien été uploadé et notre composant fonctionne, vous pouvez copier coller cette approche pour l'utiliser facilement dans vos projets.
Bon code.

Alexandre P.
Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.
Poursuivre la lecture dans la rubrique Dev