Construire une Application CRUD Full-Stack avec MongoDB, Mongoose et Next.js 15

MongoDB est la base de données NoSQL la plus populaire au monde — et Mongoose la rend typée et élégante en TypeScript. Dans ce tutoriel, vous allez construire une application complète de gestion de tâches depuis zéro avec MongoDB Atlas, Mongoose ODM et Next.js 15 App Router avec Server Actions.
Ce que vous allez apprendre
À la fin de ce tutoriel, vous serez capable de :
- Configurer un cluster MongoDB Atlas et le connecter à Next.js
- Définir des schémas Mongoose avec inférence de types TypeScript
- Construire des opérations CRUD complètes avec les Server Actions de Next.js
- Implémenter la validation de formulaires avec Zod
- Ajouter la recherche et le filtrage avec les requêtes MongoDB
- Gérer la pagination avec des patterns offset
- Déployer en production avec les bonnes pratiques de gestion des connexions
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - De l'expérience en TypeScript (types, interfaces, async/await)
- Une familiarité avec Next.js (App Router, Server Components)
- Un compte MongoDB Atlas (le tier gratuit fonctionne parfaitement)
- Un éditeur de code — VS Code ou Cursor recommandé
Pourquoi MongoDB + Mongoose ?
MongoDB est une base de données de documents qui stocke les données dans des documents flexibles de type JSON. Combiné avec Mongoose, vous obtenez la validation de schéma, la sécurité des types et une API de requêtes puissante. Voici la comparaison :
| Fonctionnalité | MongoDB + Mongoose | PostgreSQL + Prisma | SQLite + Drizzle |
|---|---|---|---|
| Modèle de données | Documents (JSON) | Tables relationnelles | Tables relationnelles |
| Schéma | Flexible, optionnel | Strict, requis | Strict, requis |
| Sécurité des types | Mongoose + TS generics | Types auto-générés | Types inférés |
| Mise à l'échelle | Horizontale (sharding) | Verticale (read replicas) | Fichier unique |
| Données imbriquées | Intégration native | Colonnes JSON ou joins | Colonnes JSON ou joins |
| Tier gratuit | Atlas 512 Mo pour toujours | Neon 0.5 Go | Local, illimité |
Choisissez MongoDB quand vos données sont naturellement hiérarchiques, que votre schéma évolue fréquemment, ou que vous avez besoin d'une mise à l'échelle horizontale.
Étape 1 : Créer le projet Next.js
Créez un nouveau projet Next.js 15 avec TypeScript et Tailwind CSS :
npx create-next-app@latest task-manager --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd task-managerInstallez les dépendances MongoDB et Mongoose :
npm install mongoose zod
npm install -D @types/mongooseLa structure de votre projet ressemblera à ceci :
task-manager/
├── src/
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── tasks/
│ ├── lib/
│ │ ├── mongodb.ts
│ │ └── actions/
│ └── models/
│ └── task.ts
├── .env.local
└── package.json
Étape 2 : Configurer MongoDB Atlas
Créer un cluster gratuit
- Allez sur MongoDB Atlas et connectez-vous
- Cliquez sur Build a Database et sélectionnez le tier M0 Free
- Choisissez votre fournisseur cloud et la région la plus proche de vos utilisateurs
- Cliquez sur Create Deployment
Configurer les accès
- Créez un utilisateur de base de données avec un nom et un mot de passe
- Dans Network Access, ajoutez votre adresse IP ou Allow Access from Anywhere pour le développement
- Cliquez sur Connect, sélectionnez Drivers et copiez la chaîne de connexion
Ajouter la chaîne de connexion
Créez un fichier .env.local à la racine du projet :
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster>.mongodb.net/task-manager?retryWrites=true&w=majorityRemplacez <username>, <password> et <cluster> par vos identifiants Atlas réels.
Étape 3 : Créer l'utilitaire de connexion MongoDB
Les connexions MongoDB dans les environnements serverless nécessitent un traitement spécial. Vous devez mettre en cache la connexion pour éviter d'épuiser le pool de connexions.
Créez src/lib/mongodb.ts :
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI!;
if (!MONGODB_URI) {
throw new Error("Please define the MONGODB_URI environment variable in .env.local");
}
interface MongooseCache {
conn: typeof mongoose | null;
promise: Promise<typeof mongoose> | null;
}
declare global {
var mongooseCache: MongooseCache | undefined;
}
const cached: MongooseCache = global.mongooseCache ?? { conn: null, promise: null };
if (!global.mongooseCache) {
global.mongooseCache = cached;
}
export async function connectDB(): Promise<typeof mongoose> {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
maxPoolSize: 10,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((m) => m);
}
cached.conn = await cached.promise;
return cached.conn;
}Ce pattern met en cache la connexion entre les rechargements à chaud en développement et entre les invocations de fonctions en production. La variable global survit aux rechargements de modules de Next.js.
Étape 4 : Définir le schéma Mongoose
Les schémas Mongoose définissent la forme de vos documents et fournissent la validation, les valeurs par défaut et les hooks middleware.
Créez src/models/task.ts :
import mongoose, { Schema, Document, Model } from "mongoose";
export interface ITask {
title: string;
description: string;
status: "todo" | "in-progress" | "done";
priority: "low" | "medium" | "high";
dueDate?: Date;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
export interface ITaskDocument extends ITask, Document {}
const taskSchema = new Schema<ITaskDocument>(
{
title: {
type: String,
required: [true, "Title is required"],
trim: true,
maxlength: [200, "Title cannot exceed 200 characters"],
},
description: {
type: String,
required: [true, "Description is required"],
trim: true,
maxlength: [2000, "Description cannot exceed 2000 characters"],
},
status: {
type: String,
enum: ["todo", "in-progress", "done"],
default: "todo",
},
priority: {
type: String,
enum: ["low", "medium", "high"],
default: "medium",
},
dueDate: {
type: Date,
},
tags: {
type: [String],
default: [],
},
},
{
timestamps: true,
}
);
taskSchema.index({ status: 1, priority: 1 });
taskSchema.index({ title: "text", description: "text" });
taskSchema.index({ createdAt: -1 });
const Task: Model<ITaskDocument> =
mongoose.models.Task || mongoose.model<ITaskDocument>("Task", taskSchema);
export default Task;Décisions de conception clés :
timestamps: truegère automatiquementcreatedAtetupdatedAt- Les index sur
statusetpriorityaccélèrent les requêtes filtrées - L'index texte sur
titleetdescriptionactive la recherche full-text mongoose.models.Task ||empêche la recompilation du modèle pendant le rechargement à chaud
Étape 5 : Créer les schémas de validation Zod
Définissez des schémas de validation qui fonctionnent côté serveur et client.
Créez src/lib/validations/task.ts :
import { z } from "zod";
export const createTaskSchema = z.object({
title: z
.string()
.min(1, "Title is required")
.max(200, "Title cannot exceed 200 characters"),
description: z
.string()
.min(1, "Description is required")
.max(2000, "Description cannot exceed 2000 characters"),
status: z.enum(["todo", "in-progress", "done"]).default("todo"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
dueDate: z.string().optional().transform((val) => (val ? new Date(val) : undefined)),
tags: z
.string()
.optional()
.transform((val) => (val ? val.split(",").map((t) => t.trim()).filter(Boolean) : [])),
});
export const updateTaskSchema = createTaskSchema.partial();
export type CreateTaskInput = z.infer<typeof createTaskSchema>;
export type UpdateTaskInput = z.infer<typeof updateTaskSchema>;Étape 6 : Construire les Server Actions
Les Server Actions sont la façon la plus propre de gérer les soumissions de formulaires et les mutations de données dans Next.js 15. Elles s'exécutent sur le serveur et peuvent accéder directement à votre base de données.
Créez src/lib/actions/task-actions.ts :
"use server";
import { revalidatePath } from "next/cache";
import { connectDB } from "@/lib/mongodb";
import Task from "@/models/task";
import { createTaskSchema, updateTaskSchema } from "@/lib/validations/task";
export type ActionState = {
success: boolean;
message: string;
errors?: Record<string, string[]>;
};
export async function createTask(
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
await connectDB();
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
status: formData.get("status") as string,
priority: formData.get("priority") as string,
dueDate: formData.get("dueDate") as string,
tags: formData.get("tags") as string,
};
const result = createTaskSchema.safeParse(raw);
if (!result.success) {
return {
success: false,
message: "La validation a échoué",
errors: result.error.flatten().fieldErrors,
};
}
await Task.create(result.data);
revalidatePath("/tasks");
return { success: true, message: "Tâche créée avec succès" };
}
export async function updateTask(
id: string,
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
await connectDB();
const raw = {
title: formData.get("title") as string,
description: formData.get("description") as string,
status: formData.get("status") as string,
priority: formData.get("priority") as string,
dueDate: formData.get("dueDate") as string,
tags: formData.get("tags") as string,
};
const result = updateTaskSchema.safeParse(raw);
if (!result.success) {
return {
success: false,
message: "La validation a échoué",
errors: result.error.flatten().fieldErrors,
};
}
const task = await Task.findByIdAndUpdate(id, result.data, { new: true });
if (!task) {
return { success: false, message: "Tâche introuvable" };
}
revalidatePath("/tasks");
return { success: true, message: "Tâche mise à jour avec succès" };
}
export async function deleteTask(id: string): Promise<ActionState> {
await connectDB();
const task = await Task.findByIdAndDelete(id);
if (!task) {
return { success: false, message: "Tâche introuvable" };
}
revalidatePath("/tasks");
return { success: true, message: "Tâche supprimée avec succès" };
}
export async function toggleTaskStatus(
id: string,
newStatus: "todo" | "in-progress" | "done"
): Promise<ActionState> {
await connectDB();
const task = await Task.findByIdAndUpdate(id, { status: newStatus }, { new: true });
if (!task) {
return { success: false, message: "Tâche introuvable" };
}
revalidatePath("/tasks");
return { success: true, message: "Statut mis à jour" };
}Chaque action suit un pattern cohérent : connecter, valider, exécuter, revalider. Le type ActionState fournit une forme prévisible pour gérer les résultats côté client.
Étape 7 : Construire la couche d'accès aux données
Créez les fonctions de requête pour lire les données. Elles s'exécutent dans les Server Components.
Créez src/lib/actions/task-queries.ts :
import { connectDB } from "@/lib/mongodb";
import Task, { ITaskDocument } from "@/models/task";
export interface TaskFilters {
status?: string;
priority?: string;
search?: string;
page?: number;
limit?: number;
}
export interface PaginatedTasks {
tasks: SerializedTask[];
total: number;
page: number;
totalPages: number;
}
export interface SerializedTask {
_id: string;
title: string;
description: string;
status: "todo" | "in-progress" | "done";
priority: "low" | "medium" | "high";
dueDate: string | null;
tags: string[];
createdAt: string;
updatedAt: string;
}
function serializeTask(task: ITaskDocument): SerializedTask {
return {
_id: task._id.toString(),
title: task.title,
description: task.description,
status: task.status,
priority: task.priority,
dueDate: task.dueDate ? task.dueDate.toISOString() : null,
tags: task.tags,
createdAt: task.createdAt.toISOString(),
updatedAt: task.updatedAt.toISOString(),
};
}
export async function getTasks(filters: TaskFilters = {}): Promise<PaginatedTasks> {
await connectDB();
const { status, priority, search, page = 1, limit = 10 } = filters;
const query: Record<string, unknown> = {};
if (status && status !== "all") {
query.status = status;
}
if (priority && priority !== "all") {
query.priority = priority;
}
if (search) {
query.$text = { $search: search };
}
const skip = (page - 1) * limit;
const [tasks, total] = await Promise.all([
Task.find(query).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(),
Task.countDocuments(query),
]);
return {
tasks: (tasks as unknown as ITaskDocument[]).map(serializeTask),
total,
page,
totalPages: Math.ceil(total / limit),
};
}
export async function getTaskById(id: string): Promise<SerializedTask | null> {
await connectDB();
const task = await Task.findById(id).lean();
if (!task) return null;
return serializeTask(task as unknown as ITaskDocument);
}
export async function getTaskStats() {
await connectDB();
const [total, todo, inProgress, done] = await Promise.all([
Task.countDocuments(),
Task.countDocuments({ status: "todo" }),
Task.countDocuments({ status: "in-progress" }),
Task.countDocuments({ status: "done" }),
]);
return { total, todo, inProgress, done };
}La fonction serializeTask convertit les documents Mongoose en objets simples — nécessaire car les Server Components ne peuvent pas passer des instances Mongoose aux Client Components.
Étape 8 : Construire la page de liste des tâches
Créez la page principale des tâches comme Server Component.
Créez src/app/tasks/page.tsx :
import Link from "next/link";
import { getTasks, getTaskStats } from "@/lib/actions/task-queries";
import { TaskCard } from "./task-card";
import { TaskFilters } from "./task-filters";
import { Pagination } from "./pagination";
interface PageProps {
searchParams: Promise<{
status?: string;
priority?: string;
search?: string;
page?: string;
}>;
}
export default async function TasksPage({ searchParams }: PageProps) {
const params = await searchParams;
const page = parseInt(params.page || "1", 10);
const [{ tasks, total, totalPages }, stats] = await Promise.all([
getTasks({
status: params.status,
priority: params.priority,
search: params.search,
page,
limit: 10,
}),
getTaskStats(),
]);
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold">Tâches</h1>
<p className="text-gray-500 mt-1">
{stats.total} total · {stats.todo} à faire ·{" "}
{stats.inProgress} en cours · {stats.done} terminées
</p>
</div>
<Link
href="/tasks/new"
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition"
>
Nouvelle tâche
</Link>
</div>
<TaskFilters />
{tasks.length === 0 ? (
<div className="text-center py-16 text-gray-500">
<p className="text-lg">Aucune tâche trouvée</p>
<p className="mt-2">Créez votre première tâche pour commencer.</p>
</div>
) : (
<div className="space-y-4 mt-6">
{tasks.map((task) => (
<TaskCard key={task._id} task={task} />
))}
</div>
)}
{totalPages > 1 && (
<Pagination currentPage={page} totalPages={totalPages} />
)}
</div>
);
}Étape 9 : Construire le composant carte de tâche
Créez un Client Component pour chaque tâche avec basculement de statut et actions de suppression.
Créez src/app/tasks/task-card.tsx :
"use client";
import { useTransition } from "react";
import Link from "next/link";
import { deleteTask, toggleTaskStatus } from "@/lib/actions/task-actions";
import type { SerializedTask } from "@/lib/actions/task-queries";
const statusColors = {
todo: "bg-gray-100 text-gray-800",
"in-progress": "bg-blue-100 text-blue-800",
done: "bg-green-100 text-green-800",
};
const priorityColors = {
low: "bg-slate-100 text-slate-700",
medium: "bg-yellow-100 text-yellow-800",
high: "bg-red-100 text-red-800",
};
const nextStatus: Record<string, "todo" | "in-progress" | "done"> = {
todo: "in-progress",
"in-progress": "done",
done: "todo",
};
export function TaskCard({ task }: { task: SerializedTask }) {
const [isPending, startTransition] = useTransition();
const handleStatusToggle = () => {
startTransition(async () => {
await toggleTaskStatus(task._id, nextStatus[task.status]);
});
};
const handleDelete = () => {
if (!confirm("Êtes-vous sûr de vouloir supprimer cette tâche ?")) return;
startTransition(async () => {
await deleteTask(task._id);
});
};
return (
<div
className={`border rounded-lg p-4 transition ${
isPending ? "opacity-50" : ""
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<button
onClick={handleStatusToggle}
className={`px-2 py-1 rounded-full text-xs font-medium ${
statusColors[task.status]
}`}
>
{task.status}
</button>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
priorityColors[task.priority]
}`}
>
{task.priority}
</span>
</div>
<Link href={`/tasks/${task._id}`} className="group">
<h3 className="text-lg font-semibold group-hover:text-blue-600 transition">
{task.title}
</h3>
</Link>
<p className="text-gray-600 mt-1 line-clamp-2">{task.description}</p>
{task.tags.length > 0 && (
<div className="flex gap-1 mt-2">
{task.tags.map((tag) => (
<span
key={tag}
className="bg-gray-100 text-gray-600 px-2 py-0.5 rounded text-xs"
>
{tag}
</span>
))}
</div>
)}
</div>
<div className="flex items-center gap-2 ml-4">
<Link
href={`/tasks/${task._id}/edit`}
className="text-gray-400 hover:text-blue-600 transition"
>
Modifier
</Link>
<button
onClick={handleDelete}
className="text-gray-400 hover:text-red-600 transition"
>
Supprimer
</button>
</div>
</div>
</div>
);
}Étape 10 : Construire le formulaire de tâche
Créez un composant formulaire réutilisable pour la création et la modification de tâches.
Créez src/app/tasks/task-form.tsx :
"use client";
import { useActionState } from "react";
import { createTask, updateTask, ActionState } from "@/lib/actions/task-actions";
import type { SerializedTask } from "@/lib/actions/task-queries";
const initialState: ActionState = {
success: false,
message: "",
};
export function TaskForm({ task }: { task?: SerializedTask }) {
const action = task
? updateTask.bind(null, task._id)
: createTask;
const [state, formAction, isPending] = useActionState(action, initialState);
return (
<form action={formAction} className="space-y-6 max-w-2xl">
{state.message && !state.success && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{state.message}
</div>
)}
{state.success && (
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded">
{state.message}
</div>
)}
<div>
<label htmlFor="title" className="block text-sm font-medium mb-1">
Titre
</label>
<input
id="title"
name="title"
type="text"
defaultValue={task?.title}
required
className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Entrez le titre de la tâche"
/>
{state.errors?.title && (
<p className="text-red-500 text-sm mt-1">{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium mb-1">
Description
</label>
<textarea
id="description"
name="description"
defaultValue={task?.description}
required
rows={4}
className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Décrivez la tâche"
/>
{state.errors?.description && (
<p className="text-red-500 text-sm mt-1">
{state.errors.description[0]}
</p>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="status" className="block text-sm font-medium mb-1">
Statut
</label>
<select
id="status"
name="status"
defaultValue={task?.status || "todo"}
className="w-full border rounded-lg px-3 py-2"
>
<option value="todo">À faire</option>
<option value="in-progress">En cours</option>
<option value="done">Terminée</option>
</select>
</div>
<div>
<label htmlFor="priority" className="block text-sm font-medium mb-1">
Priorité
</label>
<select
id="priority"
name="priority"
defaultValue={task?.priority || "medium"}
className="w-full border rounded-lg px-3 py-2"
>
<option value="low">Basse</option>
<option value="medium">Moyenne</option>
<option value="high">Haute</option>
</select>
</div>
</div>
<div>
<label htmlFor="dueDate" className="block text-sm font-medium mb-1">
Date d'échéance (optionnelle)
</label>
<input
id="dueDate"
name="dueDate"
type="date"
defaultValue={task?.dueDate ? task.dueDate.split("T")[0] : ""}
className="w-full border rounded-lg px-3 py-2"
/>
</div>
<div>
<label htmlFor="tags" className="block text-sm font-medium mb-1">
Tags (séparés par des virgules)
</label>
<input
id="tags"
name="tags"
type="text"
defaultValue={task?.tags.join(", ")}
className="w-full border rounded-lg px-3 py-2"
placeholder="frontend, urgent, bug"
/>
</div>
<button
type="submit"
disabled={isPending}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition"
>
{isPending
? task
? "Mise à jour..."
: "Création..."
: task
? "Mettre à jour la tâche"
: "Créer la tâche"}
</button>
</form>
);
}Étape 11 : Construire le composant de filtres
Créez src/app/tasks/task-filters.tsx :
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useState } from "react";
export function TaskFilters() {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState(searchParams.get("search") || "");
const updateFilter = useCallback(
(key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value && value !== "all") {
params.set(key, value);
} else {
params.delete(key);
}
params.delete("page");
router.push(`/tasks?${params.toString()}`);
},
[router, searchParams]
);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
updateFilter("search", search);
};
return (
<div className="flex flex-wrap items-center gap-4">
<form onSubmit={handleSearch} className="flex gap-2">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Rechercher des tâches..."
className="border rounded-lg px-3 py-2 w-64"
/>
<button
type="submit"
className="bg-gray-100 px-3 py-2 rounded-lg hover:bg-gray-200 transition"
>
Rechercher
</button>
</form>
<select
value={searchParams.get("status") || "all"}
onChange={(e) => updateFilter("status", e.target.value)}
className="border rounded-lg px-3 py-2"
>
<option value="all">Tous les statuts</option>
<option value="todo">À faire</option>
<option value="in-progress">En cours</option>
<option value="done">Terminée</option>
</select>
<select
value={searchParams.get("priority") || "all"}
onChange={(e) => updateFilter("priority", e.target.value)}
className="border rounded-lg px-3 py-2"
>
<option value="all">Toutes les priorités</option>
<option value="low">Basse</option>
<option value="medium">Moyenne</option>
<option value="high">Haute</option>
</select>
</div>
);
}Étape 12 : Construire les pages de création et modification
Créez src/app/tasks/new/page.tsx :
import Link from "next/link";
import { TaskForm } from "../task-form";
export default function NewTaskPage() {
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="mb-8">
<Link href="/tasks" className="text-blue-600 hover:underline">
← Retour aux tâches
</Link>
<h1 className="text-3xl font-bold mt-4">Créer une nouvelle tâche</h1>
</div>
<TaskForm />
</div>
);
}Créez src/app/tasks/[id]/edit/page.tsx :
import Link from "next/link";
import { notFound } from "next/navigation";
import { getTaskById } from "@/lib/actions/task-queries";
import { TaskForm } from "../../task-form";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function EditTaskPage({ params }: PageProps) {
const { id } = await params;
const task = await getTaskById(id);
if (!task) notFound();
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<div className="mb-8">
<Link href="/tasks" className="text-blue-600 hover:underline">
← Retour aux tâches
</Link>
<h1 className="text-3xl font-bold mt-4">Modifier la tâche</h1>
</div>
<TaskForm task={task} />
</div>
);
}Étape 13 : Patterns avancés
Pipelines d'agrégation
Le framework d'agrégation de MongoDB est puissant pour les analyses. Voici comment obtenir les taux de complétion des tâches par priorité :
export async function getCompletionRatesByPriority() {
await connectDB();
const results = await Task.aggregate([
{
$group: {
_id: "$priority",
total: { $sum: 1 },
completed: {
$sum: { $cond: [{ $eq: ["$status", "done"] }, 1, 0] },
},
},
},
{
$project: {
priority: "$_id",
total: 1,
completed: 1,
rate: {
$round: [{ $multiply: [{ $divide: ["$completed", "$total"] }, 100] }, 1],
},
},
},
{ $sort: { rate: -1 } },
]);
return results;
}Middleware Mongoose (Hooks)
Ajoutez des hooks pre/post pour les opérations courantes :
taskSchema.pre("save", function (next) {
if (this.isModified("status") && this.status === "done") {
this.set("completedAt", new Date());
}
next();
});Champs virtuels
Ajoutez des champs calculés sans les stocker :
taskSchema.virtual("isOverdue").get(function () {
if (!this.dueDate || this.status === "done") return false;
return new Date() > this.dueDate;
});
taskSchema.set("toJSON", { virtuals: true });Bonnes pratiques pour la production
1. Gestion du pool de connexions
L'utilitaire de connexion que nous avons construit gère le pooling, mais ajustez-le pour la production :
const opts = {
bufferCommands: false,
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
};2. Gestion des index
Vérifiez toujours que vos index sont utilisés :
db.tasks.find({ status: "todo" }).explain("executionStats")3. Gestion des erreurs
Encapsulez les opérations de base de données avec une gestion d'erreurs appropriée :
export async function createTask(/* ... */): Promise<ActionState> {
try {
await connectDB();
// ... opération
} catch (error) {
if (error instanceof mongoose.Error.ValidationError) {
return { success: false, message: "Données invalides fournies" };
}
return { success: false, message: "Une erreur inattendue est survenue" };
}
}4. Déployer sur Vercel
MongoDB Atlas fonctionne parfaitement avec les fonctions serverless de Vercel :
npm i -g vercel
vercel
vercel env add MONGODB_URIDépannage
"MongoServerError: bad auth"
Les identifiants de votre chaîne de connexion sont incorrects. Vérifiez le nom d'utilisateur et le mot de passe dans Atlas sous Database Access.
"MongooseServerSelectionError: connection timed out"
Votre IP n'est pas autorisée. Allez dans Atlas Network Access et ajoutez votre IP actuelle.
"OverwriteModelError: Cannot overwrite model"
Cela arrive pendant le rechargement à chaud. Le pattern mongoose.models.Task || dans le fichier modèle empêche cela.
"buffering timed out after 10000ms"
La connexion a échoué silencieusement. Assurez-vous que MONGODB_URI est défini et que le cluster fonctionne.
Prochaines étapes
Maintenant que vous avez une application MongoDB + Next.js fonctionnelle, explorez ces améliorations :
- Authentification — Ajoutez des comptes utilisateurs avec NextAuth.js
- Mises à jour en temps réel — Utilisez MongoDB Change Streams avec les Server-Sent Events
- Pièces jointes — Stockez les fichiers dans MongoDB GridFS ou UploadThing
- Recherche avancée — Passez à MongoDB Atlas Search pour la correspondance floue
- Cache — Ajoutez un cache Redis
Conclusion
Vous avez construit une application CRUD full-stack complète avec MongoDB Atlas, Mongoose et Next.js 15 App Router. L'application inclut des schémas typés, des Server Actions pour les mutations, la validation Zod, la recherche full-text, le filtrage, la pagination et une gestion des connexions prête pour la production.
Le modèle de documents flexible de MongoDB le rend idéal pour le prototypage rapide et les applications avec des schémas évolutifs. Combiné avec la couche de validation de Mongoose et les Server Actions de Next.js, vous obtenez une expérience de développement productive et typée avec un minimum de code répétitif.
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 avec Prisma ORM et Next.js 15 App Router
Apprenez à construire une application full-stack avec Prisma ORM, Next.js 15 App Router et PostgreSQL. Ce tutoriel couvre la modélisation du schéma, les migrations, les Server Actions, les opérations CRUD, les relations et le déploiement en production.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.

Neon Serverless Postgres avec Next.js App Router : construire une application full-stack avec le branching de base de données
Apprenez à construire une application Next.js full-stack alimentée par Neon Serverless Postgres. Ce tutoriel couvre le driver serverless Neon, le branching de base de données pour les déploiements preview, le connection pooling et les patterns prêts pour la production.