Créer une Application Fullstack avec PocketBase et Next.js en 2026

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

PocketBase est un backend open source écrit en Go qui tient dans un seul fichier binaire. Il offre une base de données SQLite, une authentification intégrée, un stockage de fichiers, des abonnements temps réel et un tableau de bord d'administration — le tout sans configuration complexe. Combiné avec Next.js, il forme une stack fullstack moderne, légère et idéale pour les projets personnels, les MVP et les applications de taille moyenne.

Dans ce tutoriel, vous allez construire une application de gestion de tâches (todo app) complète avec authentification utilisateur, opérations CRUD temps réel et déploiement en production.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 18+ installé sur votre machine
  • npm ou pnpm comme gestionnaire de paquets
  • Connaissances de base en React et TypeScript
  • Un éditeur de code (VS Code recommandé)
  • Un terminal (bash, zsh ou PowerShell)

Ce que vous allez construire

Une application de gestion de tâches avec les fonctionnalités suivantes :

  • Inscription et connexion utilisateur
  • Création, lecture, mise à jour et suppression de tâches
  • Mise à jour en temps réel via les abonnements PocketBase
  • Interface responsive avec Tailwind CSS
  • Déploiement prêt pour la production

Étape 1 : Installer PocketBase

PocketBase est distribué sous forme d'un seul fichier exécutable. Téléchargez-le depuis le site officiel.

# Créer un dossier pour le backend
mkdir pocketbase-backend && cd pocketbase-backend
 
# Télécharger PocketBase (Linux/macOS)
# Visitez https://pocketbase.io/docs/ pour la dernière version
wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip
 
# Ou sur macOS avec Homebrew
brew install pocketbase
 
# Décompresser
unzip pocketbase_0.25.0_linux_amd64.zip

Lancez PocketBase :

./pocketbase serve

Vous verrez dans le terminal :

> Server started at: http://127.0.0.1:8090
> - REST API: http://127.0.0.1:8090/api/
> - Admin UI: http://127.0.0.1:8090/_/

Ouvrez http://127.0.0.1:8090/_/ dans votre navigateur pour accéder au tableau de bord d'administration. Lors du premier accès, créez un compte administrateur.

Étape 2 : Configurer les collections PocketBase

Dans le tableau de bord admin, créez une collection tasks avec les champs suivants :

ChampTypeOptions
titleTextRequis, max 200 caractères
descriptionTextOptionnel
completedBoolValeur par défaut : false
userRelationCollection : users, requis

Configurer les règles d'accès

Dans l'onglet API Rules de la collection tasks :

  • List/Search : @request.auth.id != "" && user = @request.auth.id
  • View : @request.auth.id != "" && user = @request.auth.id
  • Create : @request.auth.id != ""
  • Update : @request.auth.id != "" && user = @request.auth.id
  • Delete : @request.auth.id != "" && user = @request.auth.id

Ces règles garantissent que chaque utilisateur ne peut voir et modifier que ses propres tâches.

Étape 3 : Créer le projet Next.js

Ouvrez un nouveau terminal et créez le projet frontend :

npx create-next-app@latest pocketbase-todo --typescript --tailwind --app --src-dir --use-npm
cd pocketbase-todo

Installez le SDK PocketBase :

npm install pocketbase

Étape 4 : Configurer le client PocketBase

Créez le fichier de configuration du client PocketBase :

// src/lib/pocketbase.ts
import PocketBase from "pocketbase";
 
const pb = new PocketBase("http://127.0.0.1:8090");
 
// Désactiver l'auto-annulation pour éviter les conflits avec React
pb.autoCancellation(false);
 
export default pb;

Créez les types TypeScript pour vos données :

// src/types/index.ts
export interface Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  user: string;
  created: string;
  updated: string;
}
 
export interface User {
  id: string;
  email: string;
  name: string;
  avatar: string;
}

Étape 5 : Créer le contexte d'authentification

Créez un provider React pour gérer l'état d'authentification globalement :

// src/contexts/AuthContext.tsx
"use client";
 
import {
  createContext,
  useContext,
  useEffect,
  useState,
  useCallback,
  type ReactNode,
} from "react";
import pb from "@/lib/pocketbase";
import type { User } from "@/types";
 
interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
  logout: () => void;
}
 
const AuthContext = createContext<AuthContextType | undefined>(undefined);
 
export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
 
  useEffect(() => {
    // Vérifier si l'utilisateur est déjà connecté
    if (pb.authStore.isValid) {
      const model = pb.authStore.model;
      if (model) {
        setUser({
          id: model.id,
          email: model.email,
          name: model.name || "",
          avatar: model.avatar || "",
        });
      }
    }
    setIsLoading(false);
 
    // Écouter les changements d'authentification
    const unsubscribe = pb.authStore.onChange((_token, model) => {
      if (model) {
        setUser({
          id: model.id,
          email: model.email,
          name: model.name || "",
          avatar: model.avatar || "",
        });
      } else {
        setUser(null);
      }
    });
 
    return () => unsubscribe();
  }, []);
 
  const login = useCallback(async (email: string, password: string) => {
    await pb.collection("users").authWithPassword(email, password);
  }, []);
 
  const register = useCallback(
    async (email: string, password: string, name: string) => {
      await pb.collection("users").create({
        email,
        password,
        passwordConfirm: password,
        name,
      });
      // Connexion automatique après inscription
      await pb.collection("users").authWithPassword(email, password);
    },
    []
  );
 
  const logout = useCallback(() => {
    pb.authStore.clear();
    setUser(null);
  }, []);
 
  return (
    <AuthContext.Provider value={{ user, isLoading, login, register, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
 
export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

Étape 6 : Créer le hook de gestion des tâches

Ce hook personnalisé encapsule toute la logique CRUD et les abonnements temps réel :

// src/hooks/useTasks.ts
"use client";
 
import { useState, useEffect, useCallback } from "react";
import pb from "@/lib/pocketbase";
import type { Task } from "@/types";
import { useAuth } from "@/contexts/AuthContext";
 
export function useTasks() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const { user } = useAuth();
 
  // Charger les tâches
  const fetchTasks = useCallback(async () => {
    if (!user) return;
    try {
      setIsLoading(true);
      const records = await pb.collection("tasks").getFullList<Task>({
        sort: "-created",
        filter: `user = "${user.id}"`,
      });
      setTasks(records);
    } catch (error) {
      console.error("Erreur lors du chargement des tâches:", error);
    } finally {
      setIsLoading(false);
    }
  }, [user]);
 
  // Abonnement temps réel
  useEffect(() => {
    if (!user) return;
 
    fetchTasks();
 
    // S'abonner aux changements de la collection
    pb.collection("tasks").subscribe<Task>("*", (event) => {
      switch (event.action) {
        case "create":
          setTasks((prev) => [event.record, ...prev]);
          break;
        case "update":
          setTasks((prev) =>
            prev.map((task) =>
              task.id === event.record.id ? event.record : task
            )
          );
          break;
        case "delete":
          setTasks((prev) =>
            prev.filter((task) => task.id !== event.record.id)
          );
          break;
      }
    });
 
    return () => {
      pb.collection("tasks").unsubscribe("*");
    };
  }, [user, fetchTasks]);
 
  // Créer une tâche
  const createTask = useCallback(
    async (title: string, description: string = "") => {
      if (!user) return;
      await pb.collection("tasks").create({
        title,
        description,
        completed: false,
        user: user.id,
      });
    },
    [user]
  );
 
  // Basculer l'état d'une tâche
  const toggleTask = useCallback(async (task: Task) => {
    await pb.collection("tasks").update(task.id, {
      completed: !task.completed,
    });
  }, []);
 
  // Supprimer une tâche
  const deleteTask = useCallback(async (taskId: string) => {
    await pb.collection("tasks").delete(taskId);
  }, []);
 
  // Mettre à jour une tâche
  const updateTask = useCallback(
    async (taskId: string, data: Partial<Task>) => {
      await pb.collection("tasks").update(taskId, data);
    },
    []
  );
 
  return {
    tasks,
    isLoading,
    createTask,
    toggleTask,
    deleteTask,
    updateTask,
    refetch: fetchTasks,
  };
}

Étape 7 : Construire les composants UI

Formulaire de connexion/inscription

// src/components/AuthForm.tsx
"use client";
 
import { useState, type FormEvent } from "react";
import { useAuth } from "@/contexts/AuthContext";
 
export default function AuthForm() {
  const [isLogin, setIsLogin] = useState(true);
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [name, setName] = useState("");
  const [error, setError] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);
  const { login, register } = useAuth();
 
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    setError("");
    setIsSubmitting(true);
 
    try {
      if (isLogin) {
        await login(email, password);
      } else {
        await register(email, password, name);
      }
    } catch (err: unknown) {
      const message =
        err instanceof Error ? err.message : "Une erreur est survenue";
      setError(message);
    } finally {
      setIsSubmitting(false);
    }
  };
 
  return (
    <div className="mx-auto max-w-md rounded-xl bg-white p-8 shadow-lg">
      <h2 className="mb-6 text-center text-2xl font-bold text-gray-800">
        {isLogin ? "Connexion" : "Inscription"}
      </h2>
 
      {error && (
        <div className="mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-600">
          {error}
        </div>
      )}
 
      <form onSubmit={handleSubmit} className="space-y-4">
        {!isLogin && (
          <input
            type="text"
            placeholder="Votre nom"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
            required
          />
        )}
        <input
          type="email"
          placeholder="Adresse e-mail"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
          required
        />
        <input
          type="password"
          placeholder="Mot de passe"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          className="w-full rounded-lg border px-4 py-3 focus:border-blue-500 focus:outline-none"
          required
          minLength={8}
        />
        <button
          type="submit"
          disabled={isSubmitting}
          className="w-full rounded-lg bg-blue-600 py-3 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
        >
          {isSubmitting
            ? "Chargement..."
            : isLogin
              ? "Se connecter"
              : "Créer un compte"}
        </button>
      </form>
 
      <p className="mt-4 text-center text-sm text-gray-600">
        {isLogin ? "Pas encore de compte ?" : "Déjà un compte ?"}
        <button
          onClick={() => setIsLogin(!isLogin)}
          className="ml-1 font-medium text-blue-600 hover:underline"
        >
          {isLogin ? "S'inscrire" : "Se connecter"}
        </button>
      </p>
    </div>
  );
}

Composant de liste de tâches

// src/components/TaskList.tsx
"use client";
 
import { useState, type FormEvent } from "react";
import { useTasks } from "@/hooks/useTasks";
import { useAuth } from "@/contexts/AuthContext";
import type { Task } from "@/types";
 
function TaskItem({
  task,
  onToggle,
  onDelete,
}: {
  task: Task;
  onToggle: (task: Task) => void;
  onDelete: (id: string) => void;
}) {
  return (
    <div className="group flex items-center gap-3 rounded-lg border bg-white p-4 transition hover:shadow-md">
      <button
        onClick={() => onToggle(task)}
        className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 transition ${
          task.completed
            ? "border-green-500 bg-green-500 text-white"
            : "border-gray-300 hover:border-blue-400"
        }`}
      >
        {task.completed && (
          <svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
          </svg>
        )}
      </button>
 
      <div className="flex-1">
        <h3
          className={`font-medium ${
            task.completed ? "text-gray-400 line-through" : "text-gray-800"
          }`}
        >
          {task.title}
        </h3>
        {task.description && (
          <p className="mt-1 text-sm text-gray-500">{task.description}</p>
        )}
      </div>
 
      <button
        onClick={() => onDelete(task.id)}
        className="rounded-lg p-2 text-gray-400 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
      >
        <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
      </button>
    </div>
  );
}
 
export default function TaskList() {
  const [newTitle, setNewTitle] = useState("");
  const [newDescription, setNewDescription] = useState("");
  const { tasks, isLoading, createTask, toggleTask, deleteTask } = useTasks();
  const { user, logout } = useAuth();
 
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    if (!newTitle.trim()) return;
    await createTask(newTitle.trim(), newDescription.trim());
    setNewTitle("");
    setNewDescription("");
  };
 
  const completedCount = tasks.filter((t) => t.completed).length;
 
  return (
    <div className="mx-auto max-w-2xl">
      {/* En-tête */}
      <div className="mb-8 flex items-center justify-between">
        <div>
          <h1 className="text-3xl font-bold text-gray-800">Mes Tâches</h1>
          <p className="mt-1 text-gray-500">
            Bonjour, {user?.name || user?.email}
          </p>
        </div>
        <button
          onClick={logout}
          className="rounded-lg px-4 py-2 text-sm text-gray-600 transition hover:bg-gray-100"
        >
          Déconnexion
        </button>
      </div>
 
      {/* Formulaire d'ajout */}
      <form onSubmit={handleSubmit} className="mb-6 space-y-3">
        <input
          type="text"
          placeholder="Ajouter une nouvelle tâche..."
          value={newTitle}
          onChange={(e) => setNewTitle(e.target.value)}
          className="w-full rounded-xl border-2 border-gray-200 px-4 py-3 text-lg focus:border-blue-500 focus:outline-none"
        />
        <div className="flex gap-3">
          <input
            type="text"
            placeholder="Description (optionnel)"
            value={newDescription}
            onChange={(e) => setNewDescription(e.target.value)}
            className="flex-1 rounded-lg border px-4 py-2 focus:border-blue-500 focus:outline-none"
          />
          <button
            type="submit"
            disabled={!newTitle.trim()}
            className="rounded-lg bg-blue-600 px-6 py-2 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
          >
            Ajouter
          </button>
        </div>
      </form>
 
      {/* Statistiques */}
      <div className="mb-4 flex gap-4 text-sm text-gray-500">
        <span>{tasks.length} tâche(s) au total</span>
        <span>{completedCount} terminée(s)</span>
        <span>{tasks.length - completedCount} en cours</span>
      </div>
 
      {/* Liste des tâches */}
      {isLoading ? (
        <div className="py-12 text-center text-gray-400">
          Chargement des tâches...
        </div>
      ) : tasks.length === 0 ? (
        <div className="py-12 text-center text-gray-400">
          <p className="text-lg">Aucune tâche pour le moment</p>
          <p className="mt-2 text-sm">
            Créez votre première tâche ci-dessus
          </p>
        </div>
      ) : (
        <div className="space-y-2">
          {tasks.map((task) => (
            <TaskItem
              key={task.id}
              task={task}
              onToggle={toggleTask}
              onDelete={deleteTask}
            />
          ))}
        </div>
      )}
    </div>
  );
}

Étape 8 : Assembler la page principale

Intégrez le provider d'authentification dans le layout :

// src/app/layout.tsx
import type { Metadata } from "next";
import { AuthProvider } from "@/contexts/AuthContext";
import "./globals.css";
 
export const metadata: Metadata = {
  title: "Todo App - PocketBase + Next.js",
  description: "Application de gestion de tâches avec PocketBase et Next.js",
};
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="fr">
      <body>
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

Créez la page principale qui affiche soit le formulaire d'authentification, soit la liste des tâches :

// src/app/page.tsx
"use client";
 
import AuthForm from "@/components/AuthForm";
import TaskList from "@/components/TaskList";
import { useAuth } from "@/contexts/AuthContext";
 
export default function Home() {
  const { user, isLoading } = useAuth();
 
  if (isLoading) {
    return (
      <div className="flex min-h-screen items-center justify-center bg-gray-50">
        <div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
      </div>
    );
  }
 
  return (
    <main className="min-h-screen bg-gray-50 px-4 py-12">
      {user ? <TaskList /> : <AuthForm />}
    </main>
  );
}

Étape 9 : Variables d'environnement

Créez un fichier .env.local pour configurer l'URL de PocketBase :

NEXT_PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090

Mettez à jour le client PocketBase pour utiliser cette variable :

// src/lib/pocketbase.ts
import PocketBase from "pocketbase";
 
const pb = new PocketBase(
  process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"
);
 
pb.autoCancellation(false);
 
export default pb;

Étape 10 : Lancer l'application

Ouvrez deux terminaux :

# Terminal 1 : PocketBase
cd pocketbase-backend
./pocketbase serve
 
# Terminal 2 : Next.js
cd pocketbase-todo
npm run dev

Ouvrez http://localhost:3000 dans votre navigateur. Vous devriez voir le formulaire de connexion. Créez un compte, puis commencez à ajouter des tâches.

Fonctionnalités avancées

Filtrage et recherche

Ajoutez un système de filtrage à votre liste de tâches :

// Dans useTasks.ts, ajoutez une fonction de recherche
const searchTasks = useCallback(
  async (query: string) => {
    if (!user) return;
    const records = await pb.collection("tasks").getFullList<Task>({
      sort: "-created",
      filter: `user = "${user.id}" && title ~ "${query}"`,
    });
    setTasks(records);
  },
  [user]
);

Pagination

Pour les applications avec beaucoup de données, utilisez la pagination :

const fetchTasksPaginated = useCallback(
  async (page: number = 1, perPage: number = 20) => {
    if (!user) return;
    const result = await pb.collection("tasks").getList<Task>(page, perPage, {
      sort: "-created",
      filter: `user = "${user.id}"`,
    });
    return {
      items: result.items,
      totalPages: result.totalPages,
      totalItems: result.totalItems,
    };
  },
  [user]
);

Upload de fichiers

PocketBase gère nativement les fichiers. Voici comment ajouter des pièces jointes aux tâches :

const createTaskWithFile = useCallback(
  async (title: string, file: File) => {
    if (!user) return;
    const formData = new FormData();
    formData.append("title", title);
    formData.append("user", user.id);
    formData.append("completed", "false");
    formData.append("attachment", file);
 
    await pb.collection("tasks").create(formData);
  },
  [user]
);

Déploiement en production

Déployer PocketBase

PocketBase peut être déployé sur n'importe quel serveur Linux :

# Sur le serveur
mkdir -p /opt/pocketbase
cd /opt/pocketbase
 
# Télécharger et extraire PocketBase
wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip
unzip pocketbase_0.25.0_linux_amd64.zip
 
# Créer un service systemd
sudo tee /etc/systemd/system/pocketbase.service > /dev/null << 'EOF'
[Unit]
Description=PocketBase
After=network.target
 
[Service]
Type=simple
User=root
ExecStart=/opt/pocketbase/pocketbase serve --http="0.0.0.0:8090"
Restart=on-failure
RestartSec=5s
 
[Install]
WantedBy=multi-user.target
EOF
 
# Activer et démarrer le service
sudo systemctl enable pocketbase
sudo systemctl start pocketbase

Déployer Next.js

Déployez le frontend sur Vercel, Netlify ou votre propre serveur :

# Mettre à jour l'URL PocketBase pour la production
# .env.production
NEXT_PUBLIC_POCKETBASE_URL=https://api.votre-domaine.com
 
# Build de production
npm run build
npm start

Configuration Nginx (reverse proxy)

server {
    listen 80;
    server_name api.votre-domaine.com;
 
    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
 
        # WebSocket support pour le temps réel
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Dépannage

Erreur CORS

Si vous rencontrez des erreurs CORS, lancez PocketBase avec l'option origins :

./pocketbase serve --origins="http://localhost:3000,https://votre-domaine.com"

Erreur de connexion au WebSocket

Assurez-vous que votre reverse proxy est configuré pour supporter les WebSockets (voir la configuration Nginx ci-dessus avec les en-têtes Upgrade et Connection).

Les données ne se mettent pas à jour en temps réel

Vérifiez que :

  1. Le client PocketBase est configuré avec autoCancellation(false)
  2. L'abonnement (subscribe) est correctement initialisé
  3. Le nettoyage (unsubscribe) est fait dans le return du useEffect

Prochaines étapes

Maintenant que votre application fonctionne, voici quelques idées pour aller plus loin :

  • Ajouter des catégories : créez une collection "categories" et liez-la aux tâches
  • Implémenter le drag-and-drop : utilisez @dnd-kit/core pour réordonner les tâches
  • Ajouter des notifications : envoyez des rappels par email via les hooks PocketBase
  • Mode hors ligne : utilisez un service worker pour permettre l'utilisation sans connexion
  • Tests E2E : ajoutez des tests avec Playwright pour valider les flux utilisateur

Conclusion

Vous avez construit une application fullstack complète avec PocketBase et Next.js. PocketBase offre une alternative légère et puissante aux backends traditionnels, avec des fonctionnalités comme l'authentification, le temps réel et le stockage de fichiers — le tout dans un seul binaire.

La combinaison PocketBase + Next.js est particulièrement adaptée pour :

  • Les prototypes rapides et les MVP
  • Les applications personnelles et les side projects
  • Les petites et moyennes applications en production
  • Les développeurs souhaitant contrôler entièrement leur stack

Le code source complet de ce tutoriel est disponible pour référence et peut être adapté à vos propres projets.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Maîtriser la prise de notes avec FlutterFlow et Supabase : Un guide complet.

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 une application full-stack en temps réel avec Convex et Next.js 15

Apprenez à construire une application full-stack en temps réel avec Convex et Next.js 15. Ce tutoriel couvre la conception de schémas, les requêtes, les mutations, les abonnements en temps réel, l'authentification et le téléchargement de fichiers — le tout avec une sécurité de types de bout en bout.

30 min read·

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·