Créer une application web full-stack avec Deno 2 et le framework Fresh

Ce que vous allez construire
Dans ce tutoriel, vous allez créer une application de gestion de tâches complète avec Deno 2 et le framework Fresh. À la fin, vous aurez une application fonctionnelle avec :
- Des pages rendues côté serveur sans JavaScript par défaut
- Des îlots interactifs (Islands) pour les composants dynamiques
- Des routes API RESTful pour les opérations CRUD
- Deno KV pour le stockage persistant des données
- TypeScript partout — aucune configuration nécessaire
Temps requis : 45-60 minutes
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Deno 2 installé — Exécutez
deno --versionpour vérifier (doit être 2.x+) - Connaissances de base en TypeScript/JavaScript
- Familiarité avec HTML et CSS
- Un éditeur de code (VS Code avec l'extension Deno recommandé)
Si Deno n'est pas installé :
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iexPourquoi Deno 2 + Fresh ?
Qu'est-ce que Deno 2 ?
Deno 2 est le runtime JavaScript et TypeScript de nouvelle génération créé par Ryan Dahl — le créateur original de Node.js. Il corrige de nombreuses erreurs de conception de Node tout en ajoutant :
- Support TypeScript natif — pas besoin de
tsconfig.jsonni d'étape de build - Sécurisé par défaut — permissions explicites pour les fichiers, le réseau et l'environnement
- Compatibilité npm — utilisez n'importe quel paquet npm avec le spécificateur
npm: - Outillage intégré — formateur, linter, lanceur de tests et benchmarker
- Deno KV — une base de données clé-valeur intégrée
Qu'est-ce que Fresh ?
Fresh est le framework web full-stack le plus populaire pour Deno. Il se distingue par :
- L'architecture Islands — n'envoie du JavaScript que pour les composants interactifs
- Pas d'étape de build — le code s'exécute directement, permettant des déploiements instantanés
- Rendu côté serveur — les pages sont pré-rendues sur le serveur pour un chargement rapide
- Routage basé sur les fichiers — les routes correspondent directement aux chemins de fichiers
- Preact sous le capot — bibliothèque UI légère compatible avec React
Étape 1 : Créer un nouveau projet Fresh
Ouvrez votre terminal et créez un nouveau projet Fresh :
deno run -A -r https://fresh.deno.dev my-task-managerLorsqu'on vous demande :
- Souhaitez-vous utiliser une bibliothèque de style ? → Sélectionnez
Tailwind CSS - Souhaitez-vous utiliser VS Code ? → Sélectionnez
Yes(si vous utilisez VS Code)
Naviguez dans le projet :
cd my-task-managerLancez le serveur de développement pour vérifier :
deno task devOuvrez http://localhost:8000 dans votre navigateur. Vous devriez voir la page d'accueil Fresh.
Structure du projet
Voici ce que Fresh a généré :
my-task-manager/
├── components/ # Composants UI partagés
├── islands/ # Composants interactifs côté client
├── routes/ # Routes basées sur les fichiers et endpoints API
│ ├── _app.tsx # Wrapper de l'application (layout)
│ ├── index.tsx # Page d'accueil
│ └── api/ # Routes API
├── static/ # Fichiers statiques (CSS, images)
├── deno.json # Configuration Deno
├── dev.ts # Point d'entrée développement
├── main.ts # Point d'entrée production
└── fresh.gen.ts # Manifeste auto-généré
Concept clé : Les fichiers dans routes/ sont rendus côté serveur par défaut. Les fichiers dans islands/ sont hydratés côté client avec JavaScript. C'est l'architecture Islands — seules les parties interactives envoient du JS au navigateur.
Étape 2 : Définir le modèle de données
Créez un nouveau fichier pour les types de tâches et les opérations de données.
Créez utils/db.ts :
// utils/db.ts
export interface Task {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: string;
updatedAt: string;
}
// Ouvrir une instance de base de données Deno KV
const kv = await Deno.openKv();
export async function getAllTasks(): Promise<Task[]> {
const tasks: Task[] = [];
const entries = kv.list<Task>({ prefix: ["tasks"] });
for await (const entry of entries) {
tasks.push(entry.value);
}
// Trier par date de création, le plus récent en premier
return tasks.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
}
export async function getTask(id: string): Promise<Task | null> {
const entry = await kv.get<Task>(["tasks", id]);
return entry.value;
}
export async function createTask(
title: string,
description: string
): Promise<Task> {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const task: Task = {
id,
title,
description,
completed: false,
createdAt: now,
updatedAt: now,
};
await kv.set(["tasks", id], task);
return task;
}
export async function updateTask(
id: string,
updates: Partial<Pick<Task, "title" | "description" | "completed">>
): Promise<Task | null> {
const existing = await getTask(id);
if (!existing) return null;
const updated: Task = {
...existing,
...updates,
updatedAt: new Date().toISOString(),
};
await kv.set(["tasks", id], updated);
return updated;
}
export async function deleteTask(id: string): Promise<boolean> {
const existing = await getTask(id);
if (!existing) return false;
await kv.delete(["tasks", id]);
return true;
}Pourquoi Deno KV ? C'est un magasin clé-valeur intégré qui ne nécessite aucune configuration. Les données persistent après le redémarrage du serveur localement, et sur Deno Deploy, elles deviennent une base de données distribuée mondialement.
Étape 3 : Créer les routes API
Fresh utilise le routage basé sur les fichiers pour les endpoints API également.
Lister et créer des tâches
Créez routes/api/tasks.ts :
// routes/api/tasks.ts
import { Handlers } from "$fresh/server.ts";
import { createTask, getAllTasks } from "../../utils/db.ts";
export const handler: Handlers = {
// GET /api/tasks — lister toutes les tâches
async GET(_req, _ctx) {
const tasks = await getAllTasks();
return new Response(JSON.stringify(tasks), {
headers: { "Content-Type": "application/json" },
});
},
// POST /api/tasks — créer une nouvelle tâche
async POST(req, _ctx) {
const body = await req.json();
const { title, description } = body;
if (!title || typeof title !== "string") {
return new Response(
JSON.stringify({ error: "Le titre est requis" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const task = await createTask(title, description || "");
return new Response(JSON.stringify(task), {
status: 201,
headers: { "Content-Type": "application/json" },
});
},
};Mettre à jour et supprimer une tâche
Créez routes/api/tasks/[id].ts :
// routes/api/tasks/[id].ts
import { Handlers } from "$fresh/server.ts";
import { deleteTask, getTask, updateTask } from "../../../utils/db.ts";
export const handler: Handlers = {
// GET /api/tasks/:id
async GET(_req, ctx) {
const task = await getTask(ctx.params.id);
if (!task) {
return new Response(
JSON.stringify({ error: "Tâche non trouvée" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify(task), {
headers: { "Content-Type": "application/json" },
});
},
// PATCH /api/tasks/:id
async PATCH(req, ctx) {
const body = await req.json();
const task = await updateTask(ctx.params.id, body);
if (!task) {
return new Response(
JSON.stringify({ error: "Tâche non trouvée" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify(task), {
headers: { "Content-Type": "application/json" },
});
},
// DELETE /api/tasks/:id
async DELETE(_req, ctx) {
const success = await deleteTask(ctx.params.id);
if (!success) {
return new Response(
JSON.stringify({ error: "Tâche non trouvée" }),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
},
};Étape 4 : Créer l'îlot de la liste de tâches
Les îlots (Islands) sont la manière dont Fresh ajoute de l'interactivité. Seuls les composants Islands envoient du JavaScript au navigateur — tout le reste reste du HTML statique.
Créez islands/TaskList.tsx :
// islands/TaskList.tsx
import { useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
interface Task {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: string;
}
export default function TaskList() {
const tasks = useSignal<Task[]>([]);
const newTitle = useSignal("");
const newDescription = useSignal("");
const loading = useSignal(true);
const error = useSignal("");
// Charger les tâches au montage
useEffect(() => {
fetchTasks();
}, []);
async function fetchTasks() {
loading.value = true;
try {
const res = await fetch("/api/tasks");
tasks.value = await res.json();
} catch (e) {
error.value = "Échec du chargement des tâches";
} finally {
loading.value = false;
}
}
async function addTask(e: Event) {
e.preventDefault();
if (!newTitle.value.trim()) return;
try {
const res = await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: newTitle.value,
description: newDescription.value,
}),
});
if (res.ok) {
newTitle.value = "";
newDescription.value = "";
await fetchTasks();
}
} catch (e) {
error.value = "Échec de l'ajout de la tâche";
}
}
async function toggleTask(id: string, completed: boolean) {
try {
await fetch(`/api/tasks/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed: !completed }),
});
await fetchTasks();
} catch (e) {
error.value = "Échec de la mise à jour";
}
}
async function removeTask(id: string) {
try {
await fetch(`/api/tasks/${id}`, { method: "DELETE" });
await fetchTasks();
} catch (e) {
error.value = "Échec de la suppression";
}
}
return (
<div class="max-w-2xl mx-auto p-4">
{/* Formulaire d'ajout */}
<form onSubmit={addTask} class="mb-8 bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold mb-4 text-gray-800">Ajouter une tâche</h2>
<input
type="text"
placeholder="Titre de la tâche..."
value={newTitle.value}
onInput={(e) => newTitle.value = (e.target as HTMLInputElement).value}
class="w-full p-3 border border-gray-300 rounded-lg mb-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required
/>
<textarea
placeholder="Description (optionnel)..."
value={newDescription.value}
onInput={(e) => newDescription.value = (e.target as HTMLTextAreaElement).value}
class="w-full p-3 border border-gray-300 rounded-lg mb-3 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={2}
/>
<button
type="submit"
class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
Ajouter la tâche
</button>
</form>
{/* Message d'erreur */}
{error.value && (
<div class="bg-red-100 text-red-700 p-3 rounded-lg mb-4">
{error.value}
</div>
)}
{/* État de chargement */}
{loading.value && (
<p class="text-center text-gray-500">Chargement des tâches...</p>
)}
{/* Liste des tâches */}
{!loading.value && tasks.value.length === 0 && (
<p class="text-center text-gray-500 py-8">
Aucune tâche pour le moment. Ajoutez votre première tâche !
</p>
)}
<div class="space-y-3">
{tasks.value.map((task) => (
<div
key={task.id}
class={`bg-white rounded-lg shadow p-4 flex items-start gap-3 transition-opacity ${
task.completed ? "opacity-60" : ""
}`}
>
<button
onClick={() => toggleTask(task.id, task.completed)}
class={`mt-1 w-5 h-5 rounded border-2 flex-shrink-0 flex items-center justify-center ${
task.completed
? "bg-green-500 border-green-500 text-white"
: "border-gray-300 hover:border-blue-500"
}`}
>
{task.completed && "✓"}
</button>
<div class="flex-1 min-w-0">
<h3
class={`font-semibold text-gray-800 ${
task.completed ? "line-through" : ""
}`}
>
{task.title}
</h3>
{task.description && (
<p class="text-gray-500 text-sm mt-1">{task.description}</p>
)}
<p class="text-gray-400 text-xs mt-1">
{new Date(task.createdAt).toLocaleDateString("fr-FR")}
</p>
</div>
<button
onClick={() => removeTask(task.id)}
class="text-red-400 hover:text-red-600 flex-shrink-0 text-lg"
title="Supprimer la tâche"
>
×
</button>
</div>
))}
</div>
</div>
);
}Points clés :
useSignalde Preact Signals fournit un état réactif — plus performant queuseState- Ce composant vit dans
islands/donc il envoie du JavaScript au client - Tout le reste de la page reste du HTML statique
Étape 5 : Créer la page principale
Connectez maintenant l'îlot à une page rendue côté serveur.
Remplacez routes/index.tsx :
// routes/index.tsx
import { Head } from "$fresh/runtime.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
import { getAllTasks } from "../utils/db.ts";
import TaskList from "../islands/TaskList.tsx";
interface PageData {
totalTasks: number;
completedTasks: number;
}
export const handler: Handlers<PageData> = {
async GET(_req, ctx) {
const tasks = await getAllTasks();
return ctx.render({
totalTasks: tasks.length,
completedTasks: tasks.filter((t) => t.completed).length,
});
},
};
export default function Home({ data }: PageProps<PageData>) {
const { totalTasks, completedTasks } = data;
const progress = totalTasks > 0
? Math.round((completedTasks / totalTasks) * 100)
: 0;
return (
<>
<Head>
<title>Gestionnaire de tâches — Deno 2 & Fresh</title>
</Head>
<div class="min-h-screen bg-gray-100">
{/* En-tête */}
<header class="bg-white shadow-sm">
<div class="max-w-2xl mx-auto px-4 py-6">
<h1 class="text-3xl font-bold text-gray-900">
📋 Gestionnaire de tâches
</h1>
<p class="text-gray-500 mt-1">
Construit avec Deno 2 & Fresh — Rendu serveur avec des îlots interactifs
</p>
</div>
</header>
{/* Barre de statistiques — Rendu serveur, pas de JS */}
<div class="max-w-2xl mx-auto px-4 mt-6">
<div class="bg-white rounded-lg shadow p-4 flex items-center justify-between">
<div class="flex gap-6 text-sm">
<span class="text-gray-600">
Total : <strong class="text-gray-900">{totalTasks}</strong>
</span>
<span class="text-gray-600">
Terminées : <strong class="text-green-600">{completedTasks}</strong>
</span>
<span class="text-gray-600">
Restantes :{" "}
<strong class="text-blue-600">
{totalTasks - completedTasks}
</strong>
</span>
</div>
<div class="flex items-center gap-2">
<div class="w-24 bg-gray-200 rounded-full h-2">
<div
class="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<span class="text-xs text-gray-500">{progress}%</span>
</div>
</div>
</div>
{/* Contenu principal */}
<main class="py-6">
<TaskList />
</main>
{/* Pied de page */}
<footer class="text-center py-6 text-gray-400 text-sm">
<p>
Propulsé par{" "}
<a href="https://deno.com" class="text-blue-500 hover:underline">
Deno 2
</a>{" "}
&{" "}
<a href="https://fresh.deno.dev" class="text-blue-500 hover:underline">
Fresh
</a>
</p>
</footer>
</div>
</>
);
}Étape 6 : Ajouter un middleware de journalisation
Fresh prend en charge les middlewares pour les préoccupations transversales.
Créez routes/_middleware.ts :
// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";
export async function handler(req: Request, ctx: FreshContext) {
const start = Date.now();
const url = new URL(req.url);
// Traiter la requête
const resp = await ctx.next();
const duration = Date.now() - start;
const status = resp.status;
console.log(
`${req.method} ${url.pathname} — ${status} (${duration}ms)`
);
return resp;
}Étape 7 : Exécuter et tester
Lancez le serveur de développement :
deno task devOuvrez http://localhost:8000 et testez :
- Ajouter une tâche — remplissez le titre et la description, cliquez sur "Ajouter la tâche"
- Basculer la complétion — cliquez sur la case à cocher à côté d'une tâche
- Supprimer une tâche — cliquez sur le bouton ×
- Rafraîchir la page — les tâches persistent grâce à Deno KV
Tester l'API directement
# Lister les tâches
curl http://localhost:8000/api/tasks
# Créer une tâche
curl -X POST http://localhost:8000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Apprendre Deno 2", "description": "Compléter le tutoriel Fresh"}'
# Basculer une tâche (remplacez TASK_ID)
curl -X PATCH http://localhost:8000/api/tasks/TASK_ID \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Supprimer une tâche
curl -X DELETE http://localhost:8000/api/tasks/TASK_IDÉtape 8 : Déployer sur Deno Deploy
Deno Deploy est la plateforme d'hébergement naturelle pour les applications Fresh. Le déploiement est quasi instantané.
Option A : Via l'intégration GitHub
- Poussez votre projet sur GitHub
- Allez sur dash.deno.com
- Créez un nouveau projet
- Liez votre dépôt GitHub
- Définissez le point d'entrée sur
main.ts - Déployez — cela prend quelques secondes
Option B : Via la CLI
# Installer deployctl
deno install -Arf jsr:@deno/deployctl
# Déployer
deployctl deploy --project=my-task-manager --entrypoint=main.tsDeno KV sur Deploy : Lors du déploiement sur Deno Deploy, vos données KV sont automatiquement distribuées mondialement — aucune configuration de base de données ni chaîne de connexion nécessaire.
Dépannage
Erreurs "Permission refusée"
Deno est sécurisé par défaut. Si vous voyez des erreurs de permission, assurez-vous d'utiliser les flags --allow-* ou de lancer avec -A pendant le développement :
deno run -A main.ts"Module non trouvé" pour les paquets npm
Utilisez le spécificateur npm: dans vos imports :
import express from "npm:express@4";Les données Deno KV ne persistent pas
Par défaut, les données KV sont stockées dans un fichier local. Vérifiez les permissions d'écriture. Pour un chemin personnalisé :
const kv = await Deno.openKv("./data/kv.db");Ce que vous avez appris
Dans ce tutoriel, vous avez construit une application complète et appris :
- La structure d'un projet Fresh — routes, îlots, composants et utilitaires
- L'architecture Islands — envoyer du JavaScript uniquement où c'est nécessaire
- Le rendu côté serveur — pages rendues sur le serveur pour un chargement instantané
- Les routes API — endpoints RESTful avec routage basé sur les fichiers
- Deno KV — stockage persistant sans configuration
- Les middlewares — préoccupations transversales comme la journalisation
- Le déploiement — mise en production avec Deno Deploy
Prochaines étapes
- Ajouter l'authentification — utilisez Deno KV OAuth pour la connexion GitHub/Google
- Ajouter des catégories — étendez le modèle de données avec des libellés et des filtres
- Ajouter des mises à jour en temps réel — utilisez les Server-Sent Events (SSE)
- Explorer les plugins Fresh — consultez fresh.deno.dev/docs/concepts/plugins
- Lire la documentation Deno — explorez docs.deno.com
Conclusion
Deno 2 et Fresh offrent une approche simple et rafraîchissante du développement web full-stack. L'architecture Islands garantit que vos applications sont rapides par défaut, TypeScript fonctionne sans configuration, et Deno KV élimine le besoin d'une base de données externe. Si vous êtes fatigué des chaînes d'outils complexes et des frameworks lourds, Fresh mérite une considération sérieuse pour votre prochain projet.
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 web full-stack avec SolidStart : guide pratique complet
Apprenez à construire une application de gestion de tâches complète avec SolidStart et SolidJS. Ce tutoriel couvre le routage par fichiers, les fonctions serveur, le chargement réactif des données, les actions et le déploiement.

Construire une application web full-stack avec SvelteKit 2 : guide pratique complet
Apprenez à construire une application de gestion de notes complète avec SvelteKit 2. Ce tutoriel pratique couvre le routage basé sur les fichiers, le rendu côté serveur, les Form Actions, les routes API et le déploiement sur Vercel.

AI SDK 4.0 : Nouvelles Fonctionnalites et Cas d'Utilisation
Decouvrez les nouvelles fonctionnalites et cas d'utilisation d'AI SDK 4.0, incluant le support PDF, l'utilisation de l'ordinateur et plus encore.