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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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éUploadThingS3 manuelMulter + Express
Sécurité des typesTypeScript completTypes manuelsAucune
Intégration frameworkNext.js natifConfiguration manuelleExpress uniquement
URL présignéesAutomatiqueConfiguration IAM manuelleN/A
Validation des fichiersDéclarativeMiddleware personnaliséMiddleware personnalisé
Composants ReactIntégrésÀ construireÀ construire
Suivi de progressionInté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-demo

Sé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/react

Ensuite, 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_here

Le 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 construction f pour 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'authentification
  • onUploadComplete() se déclenche après le stockage du fichier — sauvegardez les métadonnées en base ici
  • satisfies FileRouter assure 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, WebPjusqu'à 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-dropzone

Cette 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 dev

Visitez 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 :

  1. Téléversement unique — cliquez sur le bouton et sélectionnez une image
  2. Téléversement multiple — sélectionnez jusqu'à 4 images à la fois
  3. Glisser-déposer — glissez des images depuis votre gestionnaire de fichiers
  4. Rejet de taille — essayez un fichier de plus de 4 Mo
  5. Mauvais type — essayez de téléverser un .txt vers le téléverseur d'images
  6. Suivi de progression — téléversez une image plus grande et observez la barre
  7. Aperçu — déposez des fichiers dans le composant personnalisé et vérifiez les miniatures
  8. 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 :

  1. Variables d'environnement — configurez UPLOADTHING_TOKEN chez votre hébergeur (Vercel, Railway, etc.)
  2. 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
  3. 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)
  4. Limitation de débit — ajoutez du rate limiting dans votre middleware pour prévenir les abus
  5. 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 onUploadComplete avec Prisma ou Drizzle
  • Authentification — intégrez NextAuth.js ou Clerk pour valider les utilisateurs dans le middleware
  • Optimisation d'images — utilisez le composant Image de 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é.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire des API REST avec Rust et Axum : guide pratique pour débutants.

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

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.

25 min read·