UploadThing + Next.js App Router : construire un système complet de téléversement avec glisser-déposer

Le téléversement de fichiers résolu pour Next.js. UploadThing est un service moderne de téléversement conçu spécifiquement pour les applications TypeScript. Dans ce tutoriel, vous construirez un système complet avec glisser-déposer, aperçus, barres de progression et validation côté serveur — le tout typé de bout en bout.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer UploadThing dans un projet Next.js 15 App Router
- Créer des routeurs de fichiers typés avec validation côté serveur
- Construire une zone de glisser-déposer avec retour visuel
- Afficher la progression en temps réel avec indicateurs de pourcentage
- Implémenter des aperçus avant et après le téléversement
- Gérer les limites de taille, restrictions de type et validation personnalisée
- Construire une galerie de fichiers pour afficher et gérer les téléversements
- Supprimer des fichiers via le programme grâce à l'API UploadThing
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - Une expérience avec Next.js (App Router, Server Components, Server Actions)
- Les bases de TypeScript (types, génériques)
- Un compte UploadThing — inscrivez-vous gratuitement sur uploadthing.com
- Un éditeur de code — VS Code ou Cursor recommandé
Pourquoi UploadThing ?
La gestion du téléversement de fichiers dans les applications web a toujours été pénible. Soit vous assemblez des URL S3 présignées avec un middleware personnalisé, soit vous utilisez des bibliothèques lourdes qui entrent en conflit avec votre framework. UploadThing adopte une approche différente :
| Fonctionnalité | UploadThing | S3 manuel | Multer + Express |
|---|---|---|---|
| Sécurité des types | TypeScript complet | Types manuels | Aucune |
| Intégration framework | Next.js natif | Configuration manuelle | Express uniquement |
| URL présignées | Automatique | Configuration IAM manuelle | N/A |
| Validation des fichiers | Déclarative | Middleware personnalisé | Middleware personnalisé |
| Composants React | Intégrés | À construire | À construire |
| Suivi de progression | Intégré | WebSocket personnalisé | Personnalisé |
Le niveau gratuit comprend 2 Go de stockage et 2 Go de bande passante mensuelle — largement suffisant pour le développement et les petits projets.
Étape 1 : Créer un projet Next.js
Commencez avec un nouveau projet Next.js 15 :
npx create-next-app@latest upload-demo --typescript --tailwind --app --src-dir
cd upload-demoSélectionnez les options par défaut. Cela vous donne un projet avec TypeScript, Tailwind CSS et App Router.
Étape 2 : Installer UploadThing
Installez le paquet principal et l'intégration React :
npm install uploadthing @uploadthing/reactEnsuite, créez un fichier .env.local avec vos identifiants UploadThing. Vous les trouverez dans le tableau de bord UploadThing après avoir créé une nouvelle application :
UPLOADTHING_TOKEN=your_token_hereLe UPLOADTHING_TOKEN est automatiquement détecté par la bibliothèque — aucune configuration supplémentaire nécessaire.
Étape 3 : Définir le routeur de fichiers
Le routeur de fichiers est le noyau d'UploadThing. Il définit les types de fichiers acceptés par votre application, leur taille maximale et ce qui se passe après le téléversement.
Créez src/app/api/uploadthing/core.ts :
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({
image: {
maxFileSize: "4MB",
maxFileCount: 4,
},
})
.middleware(async ({ req }) => {
// Exécuter la logique côté serveur avant le téléversement
// Par exemple, vérifier l'authentification
const user = { id: "user_123" }; // Remplacez par l'auth réelle
if (!user) throw new UploadThingError("Unauthorized");
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Téléversement terminé pour:", metadata.userId);
console.log("URL du fichier:", file.ufsUrl);
// Retourner les données au client
return { uploadedBy: metadata.userId, url: file.ufsUrl };
}),
documentUploader: f({
pdf: { maxFileSize: "16MB", maxFileCount: 1 },
"application/msword": { maxFileSize: "16MB", maxFileCount: 1 },
})
.middleware(async ({ req }) => {
return { uploadedAt: new Date().toISOString() };
})
.onUploadComplete(async ({ metadata, file }) => {
return { url: file.ufsUrl, uploadedAt: metadata.uploadedAt };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;Concepts clés :
createUploadthing()retourne une fonction de constructionfpour définir les routes- La config des types de fichiers spécifie les types MIME autorisés, la taille max et le nombre max
middleware()s'exécute sur le serveur avant le début du téléversement — parfait pour l'authentificationonUploadComplete()se déclenche après le stockage du fichier — sauvegardez les métadonnées en base icisatisfies FileRouterassure la sécurité des types sur toute la chaîne
Étape 4 : Créer la route API
Créez le gestionnaire de route Next.js dans src/app/api/uploadthing/route.ts :
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});Ce fichier de deux lignes crée les gestionnaires GET et POST dont UploadThing a besoin pour négocier les téléversements avec le client.
Étape 5 : Générer les helpers React
Créez un fichier utilitaire qui génère des hooks et composants React typés depuis votre routeur de fichiers.
Créez src/utils/uploadthing.ts :
import {
generateUploadButton,
generateUploadDropzone,
generateReactHelpers,
} from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";
export const UploadButton = generateUploadButton<OurFileRouter>();
export const UploadDropzone = generateUploadDropzone<OurFileRouter>();
export const { useUploadThing } = generateReactHelpers<OurFileRouter>();Ces composants générés sont entièrement typés — votre IDE autocomplètera les noms des endpoints et connaîtra exactement les métadonnées retournées par chaque route.
Étape 6 : Ajouter les styles UploadThing
Importez les styles par défaut dans src/app/layout.tsx :
import "@uploadthing/react/styles.css";Ajoutez cette ligne à côté de vos imports CSS existants. Les styles fournissent un rendu par défaut soigné pour le bouton et la zone de dépôt.
Étape 7 : Construire le bouton de téléversement basique
Commençons par l'intégration la plus simple — un bouton stylisé.
Créez src/components/BasicUpload.tsx :
"use client";
import { UploadButton } from "@/utils/uploadthing";
import { useState } from "react";
interface UploadedFile {
url: string;
name: string;
}
export default function BasicUpload() {
const [files, setFiles] = useState<UploadedFile[]>([]);
return (
<div className="flex flex-col items-center gap-4">
<h2 className="text-xl font-bold">Téléverser une image</h2>
<UploadButton
endpoint="imageUploader"
onClientUploadComplete={(res) => {
if (res) {
const uploaded = res.map((file) => ({
url: file.ufsUrl,
name: file.name,
}));
setFiles((prev) => [...prev, ...uploaded]);
}
}}
onUploadError={(error: Error) => {
alert(`Échec du téléversement : ${error.message}`);
}}
/>
{files.length > 0 && (
<div className="grid grid-cols-2 gap-4 mt-4">
{files.map((file, i) => (
<div key={i} className="relative">
<img
src={file.url}
alt={file.name}
className="w-48 h-48 object-cover rounded-lg"
/>
<p className="text-sm text-center mt-1 truncate w-48">
{file.name}
</p>
</div>
))}
</div>
)}
</div>
);
}La prop endpoint est entièrement typée — essayez de saisir un mauvais nom et TypeScript le détectera immédiatement.
Étape 8 : Construire la zone de glisser-déposer
La dropzone offre une zone cible plus large avec support du glisser-déposer.
Créez src/components/DropzoneUpload.tsx :
"use client";
import { UploadDropzone } from "@/utils/uploadthing";
import { useState } from "react";
interface UploadedFile {
url: string;
name: string;
size: number;
}
export default function DropzoneUpload() {
const [files, setFiles] = useState<UploadedFile[]>([]);
return (
<div className="w-full max-w-xl mx-auto">
<h2 className="text-xl font-bold mb-4">Déposez vos images ici</h2>
<UploadDropzone
endpoint="imageUploader"
onClientUploadComplete={(res) => {
if (res) {
const uploaded = res.map((file) => ({
url: file.ufsUrl,
name: file.name,
size: file.size,
}));
setFiles((prev) => [...prev, ...uploaded]);
}
}}
onUploadError={(error: Error) => {
alert(`Échec du téléversement : ${error.message}`);
}}
config={{ mode: "auto" }}
/>
{files.length > 0 && (
<div className="mt-6 space-y-2">
<h3 className="font-semibold">Fichiers téléversés :</h3>
{files.map((file, i) => (
<div
key={i}
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg"
>
<img
src={file.url}
alt={file.name}
className="w-12 h-12 object-cover rounded"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-gray-500">
{(file.size / 1024).toFixed(1)} Ko
</p>
</div>
</div>
))}
</div>
)}
</div>
);
}Le paramètre config mode sur "auto" signifie que les fichiers sont téléversés immédiatement au dépôt — pas de clic supplémentaire nécessaire. Retirez cette prop pour exiger un clic manuel sur le bouton "Téléverser" après le dépôt.
Étape 9 : Construire un téléversement personnalisé avec suivi de progression
Pour un contrôle total sur l'interface, utilisez le hook useUploadThing directement.
Créez src/components/CustomUpload.tsx :
"use client";
import { useUploadThing } from "@/utils/uploadthing";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";
interface FileWithPreview extends File {
preview: string;
}
export default function CustomUpload() {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [uploadedUrls, setUploadedUrls] = useState<string[]>([]);
const [isUploading, setIsUploading] = useState(false);
const { startUpload } = useUploadThing("imageUploader", {
onUploadProgress: (progress) => {
setUploadProgress(progress);
},
onClientUploadComplete: (res) => {
if (res) {
setUploadedUrls(res.map((file) => file.ufsUrl));
}
setIsUploading(false);
setUploadProgress(0);
setFiles([]);
},
onUploadError: (error) => {
alert(`Erreur : ${error.message}`);
setIsUploading(false);
setUploadProgress(0);
},
});
const onDrop = useCallback((acceptedFiles: File[]) => {
const withPreviews = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
setFiles(withPreviews);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: { "image/*": [".png", ".jpg", ".jpeg", ".webp"] },
maxFiles: 4,
maxSize: 4 * 1024 * 1024,
});
const handleUpload = async () => {
if (files.length === 0) return;
setIsUploading(true);
await startUpload(files);
};
return (
<div className="w-full max-w-xl mx-auto space-y-4">
<h2 className="text-xl font-bold">Téléversement personnalisé avec aperçu</h2>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-colors ${
isDragActive
? "border-blue-500 bg-blue-50"
: "border-gray-300 hover:border-gray-400"
}`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p className="text-blue-600">Déposez les fichiers ici...</p>
) : (
<div>
<p className="text-gray-600">
Glissez-déposez des images ici, ou cliquez pour sélectionner
</p>
<p className="text-sm text-gray-400 mt-1">
PNG, JPG, WebP — jusqu'à 4 Mo chacun, 4 fichiers maximum
</p>
</div>
)}
</div>
{files.length > 0 && (
<div className="grid grid-cols-4 gap-2">
{files.map((file, i) => (
<div key={i} className="relative aspect-square">
<img
src={file.preview}
alt={file.name}
className="w-full h-full object-cover rounded-lg"
/>
</div>
))}
</div>
)}
{isUploading && (
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-blue-600 h-3 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
<p className="text-sm text-center mt-1">{uploadProgress}%</p>
</div>
)}
<button
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
className="w-full py-2 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isUploading ? "Téléversement..." : `Téléverser ${files.length} fichier(s)`}
</button>
{uploadedUrls.length > 0 && (
<div className="p-4 bg-green-50 rounded-lg">
<p className="font-semibold text-green-800">Téléversement terminé !</p>
{uploadedUrls.map((url, i) => (
<a
key={i}
href={url}
target="_blank"
rel="noopener noreferrer"
className="block text-sm text-green-600 hover:underline truncate"
>
{url}
</a>
))}
</div>
)}
</div>
);
}Vous devrez installer react-dropzone pour ce composant :
npm install react-dropzoneCette implémentation personnalisée vous offre :
- Aperçus avant le téléversement avec
URL.createObjectURL - Une barre de progression qui se remplit pendant le téléversement
- Glisser-déposer avec retour visuel (changement de couleur de bordure)
- Restrictions de type et taille appliquées côté client
- Contrôle total sur chaque élément de l'interface
Étape 10 : Validation côté serveur
La fonction middleware dans votre routeur est l'endroit où la validation sérieuse se produit. Voici une version améliorée avec plusieurs vérifications :
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { UploadThingError } from "uploadthing/server";
const f = createUploadthing();
async function authenticateUser(req: Request) {
// Remplacez par votre logique d'auth réelle
const token = req.headers.get("authorization");
if (!token) return null;
return { id: "user_123", plan: "pro" as const };
}
const PLAN_LIMITS = {
free: { maxSize: "2MB" as const, maxCount: 2 },
pro: { maxSize: "8MB" as const, maxCount: 10 },
};
export const ourFileRouter = {
imageUploader: f({
image: {
maxFileSize: "8MB",
maxFileCount: 10,
},
})
.middleware(async ({ req, files }) => {
const user = await authenticateUser(req);
if (!user) throw new UploadThingError("Unauthorized");
const limits = PLAN_LIMITS[user.plan];
// Valider le nombre de fichiers selon le plan
if (files.length > limits.maxCount) {
throw new UploadThingError(
`Votre plan autorise jusqu'à ${limits.maxCount} fichiers par téléversement`
);
}
return { userId: user.id, plan: user.plan };
})
.onUploadComplete(async ({ metadata, file }) => {
// Sauvegarder en base de données
// await db.insert(uploads).values({
// userId: metadata.userId,
// url: file.ufsUrl,
// name: file.name,
// size: file.size,
// });
return { url: file.ufsUrl };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;Le middleware s'exécute entièrement sur le serveur — les utilisateurs ne peuvent pas le contourner en modifiant le code client. C'est ici que vous appliquez l'authentification, les limites par plan, la limitation de débit et toute règle métier.
Étape 11 : Construire une galerie avec suppression
Un système complet nécessite la gestion des fichiers. Voici un composant galerie qui affiche les téléversements et permet la suppression.
D'abord, créez une Server Action pour la suppression dans src/app/actions.ts :
"use server";
import { UTApi } from "uploadthing/server";
const utapi = new UTApi();
export async function deleteFile(fileKey: string) {
try {
await utapi.deleteFiles(fileKey);
return { success: true };
} catch (error) {
return { success: false, error: "Échec de la suppression" };
}
}Maintenant créez src/components/FileGallery.tsx :
"use client";
import { useState } from "react";
import { deleteFile } from "@/app/actions";
interface GalleryFile {
key: string;
url: string;
name: string;
size: number;
}
export default function FileGallery({
initialFiles,
}: {
initialFiles: GalleryFile[];
}) {
const [files, setFiles] = useState<GalleryFile[]>(initialFiles);
const [deleting, setDeleting] = useState<string | null>(null);
const handleDelete = async (fileKey: string) => {
setDeleting(fileKey);
const result = await deleteFile(fileKey);
if (result.success) {
setFiles((prev) => prev.filter((f) => f.key !== fileKey));
} else {
alert("Échec de la suppression du fichier");
}
setDeleting(null);
};
if (files.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p>Aucun fichier téléversé pour le moment.</p>
</div>
);
}
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{files.map((file) => (
<div
key={file.key}
className="group relative bg-white rounded-xl shadow-sm overflow-hidden"
>
<div className="aspect-square">
<img
src={file.url}
alt={file.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-2">
<p className="text-sm font-medium truncate">{file.name}</p>
<p className="text-xs text-gray-400">
{(file.size / 1024).toFixed(1)} Ko
</p>
</div>
<button
onClick={() => handleDelete(file.key)}
disabled={deleting === file.key}
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm transition-opacity hover:bg-red-600 disabled:opacity-50"
>
{deleting === file.key ? "..." : "X"}
</button>
</div>
))}
</div>
);
}La classe UTApi fournit des méthodes côté serveur pour gérer les fichiers : lister, supprimer, renommer et obtenir les URL. Utilisez-la dans les Server Actions ou routes API — ne l'exposez jamais au client.
Étape 12 : Assembler le tout
Mettez à jour votre page principale pour présenter tous les composants.
Remplacez src/app/page.tsx :
import BasicUpload from "@/components/BasicUpload";
import DropzoneUpload from "@/components/DropzoneUpload";
import CustomUpload from "@/components/CustomUpload";
export default function Home() {
return (
<main className="min-h-screen bg-gray-50 py-12 px-4">
<div className="max-w-4xl mx-auto space-y-16">
<div className="text-center">
<h1 className="text-3xl font-bold">Démo UploadThing</h1>
<p className="text-gray-600 mt-2">
Trois façons de gérer le téléversement dans Next.js
</p>
</div>
<section className="bg-white rounded-2xl p-8 shadow-sm">
<BasicUpload />
</section>
<section className="bg-white rounded-2xl p-8 shadow-sm">
<DropzoneUpload />
</section>
<section className="bg-white rounded-2xl p-8 shadow-sm">
<CustomUpload />
</section>
</div>
</main>
);
}Lancez le serveur de développement :
npm run devVisitez http://localhost:3000 et testez chaque méthode. Essayez de glisser des images, cliquer pour sélectionner et observer la barre de progression.
Tester votre implémentation
Vérifiez que ces scénarios fonctionnent correctement :
- Téléversement unique — cliquez sur le bouton et sélectionnez une image
- Téléversement multiple — sélectionnez jusqu'à 4 images à la fois
- Glisser-déposer — glissez des images depuis votre gestionnaire de fichiers
- Rejet de taille — essayez un fichier de plus de 4 Mo
- Mauvais type — essayez de téléverser un
.txtvers le téléverseur d'images - Suivi de progression — téléversez une image plus grande et observez la barre
- Aperçu — déposez des fichiers dans le composant personnalisé et vérifiez les miniatures
- Suppression — survolez une image de la galerie et cliquez sur le bouton de suppression
Dépannage
"UPLOADTHING_TOKEN is not set"
Vérifiez que votre fichier .env.local existe à la racine du projet et contient le token. Redémarrez le serveur de développement après avoir ajouté des variables d'environnement.
Les téléversements échouent silencieusement
Vérifiez l'onglet Network du navigateur pour les requêtes vers /api/uploadthing. Problèmes courants :
- Le fichier de route API est au mauvais emplacement (doit être
app/api/uploadthing/route.ts) - Le token est expiré ou appartient à une autre application
- Problèmes CORS lors de tests depuis un autre domaine
Les fichiers se téléversent mais les URL retournent 404
Les URL UploadThing suivent le format https://ufs.sh/f/.... Si vous voyez des URL de type ancien utfs.io, mettez à jour votre paquet vers la dernière version.
Erreurs TypeScript sur les noms des endpoints
Lancez npm run dev au moins une fois après avoir modifié votre routeur. Les types sont générés depuis la définition du routeur — votre IDE a besoin du serveur de développement en cours d'exécution.
Déploiement en production
Lors du déploiement en production, gardez ces points à l'esprit :
- Variables d'environnement — configurez
UPLOADTHING_TOKENchez votre hébergeur (Vercel, Railway, etc.) - URL de callback — UploadThing doit atteindre votre serveur pour
onUploadComplete. Sur Vercel, cela fonctionne automatiquement. Pour les déploiements personnalisés, configurez l'URL dans le tableau de bord UploadThing - Nettoyage des fichiers — implémentez une tâche planifiée pour supprimer les fichiers orphelins (téléversés mais jamais enregistrés en base)
- Limitation de débit — ajoutez du rate limiting dans votre middleware pour prévenir les abus
- Modération du contenu — pour le contenu généré par les utilisateurs, envisagez d'intégrer des API de modération dans
onUploadComplete
Prochaines étapes
Maintenant que vous avez un système fonctionnel, envisagez ces améliorations :
- Persistance en base — sauvegardez les métadonnées dans votre base de données dans
onUploadCompleteavec Prisma ou Drizzle - Authentification — intégrez NextAuth.js ou Clerk pour valider les utilisateurs dans le middleware
- Optimisation d'images — utilisez le composant
Imagede Next.js avec les URL téléversées pour l'optimisation automatique - URL présignées — pour les fichiers privés, générez des URL d'accès temporaires avec
UTApi - Webhooks — configurez les webhooks UploadThing pour le traitement asynchrone (scan antivirus, génération de miniatures)
Conclusion
Vous avez construit un système complet de téléversement avec UploadThing et Next.js App Router. L'implémentation couvre trois approches — du UploadButton sans configuration à une interface glisser-déposer entièrement personnalisée avec suivi de progression et aperçus.
UploadThing supprime la complexité infrastructurelle : pas de buckets S3 à configurer, pas de politiques IAM à déboguer, pas de logique d'URL présignées à écrire. Vous définissez les fichiers acceptés, qui peut les téléverser et ce qui se passe ensuite — le reste est géré pour vous.
La sécurité des types de bout en bout signifie que votre IDE détecte les erreurs au développement, pas en production. Quand vous modifiez un nom de route, ajoutez un endpoint ou changez la forme des métadonnées, TypeScript vous guide à travers chaque fichier à mettre à jour.
Pour aller plus loin, consultez la documentation UploadThing et explorez UTApi pour les opérations avancées côté serveur comme le listage des fichiers, les statistiques d'utilisation et la génération d'URL présignées pour le contenu privé.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

Construire un Chatbot IA Local avec Ollama et Next.js : Guide Complet
Construisez un chatbot IA privé fonctionnant entièrement sur votre machine locale avec Ollama et Next.js. Ce tutoriel pratique couvre l'installation, le streaming des réponses, la sélection de modèles et le déploiement d'une interface de chat prête pour la production — le tout sans envoyer de données dans le cloud.

Construire un site web axé sur le contenu avec Payload CMS 3 et Next.js
Apprenez à construire un site web complet avec Payload CMS 3, qui fonctionne nativement dans Next.js App Router. Ce tutoriel couvre les collections, le rich text, les uploads, l'authentification et le déploiement en production.