écrits/tutorial/2026/06
Tutorial29 juin 2026·22 min

Créer des applications bureau multiplateformes avec Deno Desktop

Apprenez à créer des applications bureau natives avec la nouvelle commande deno desktop de Deno 2.9. Ce tutoriel couvre l'API BrowserWindow, le pont web-natif, les menus, l'icône de barre des tâches et la compilation multiplateforme — tout en TypeScript.

Deno 2.9 (sorti le 25 juin 2026) livre l'une des fonctionnalités les plus attendues de l'écosystème JavaScript : la commande expérimentale deno desktop, qui transforme n'importe quel projet web en application bureau native et distribuable. Pas d'Electron, pas de fichiers de configuration Tauri complexes — du TypeScript, et c'est tout.

Dans ce tutoriel, vous allez créer une application bureau de prise de notes Markdown — une interface web servie par Deno.serve(), reliée au système d'exploitation via l'API Deno.BrowserWindow, avec une barre de menus native et une icône de barre des tâches, compilée en un binaire unique pour macOS, Windows et Linux.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Deno 2.9 ou plus récent (exécutez deno upgrade si vous avez une version antérieure)
  • Des connaissances de base en TypeScript et JavaScript moderne
  • Une familiarité avec Fresh, Astro ou un serveur HTTP Deno basique
  • Un éditeur de code (VS Code avec l'extension Deno est recommandé)

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez :

  1. Une application de notes bureau avec une interface WebView
  2. Un backend TypeScript tournant dans Deno qui communique avec l'interface via window.bind()
  3. Une barre de menus native avec raccourcis clavier
  4. Une icône de barre des tâches avec un panneau contextuel
  5. Un binaire distribuable pour macOS, Windows et Linux

Étape 1 : Installer ou mettre à jour Deno 2.9

Si Deno n'est pas encore installé :

curl -fsSL https://deno.land/install.sh | sh

Si vous avez déjà Deno, mettez-le à jour vers la version 2.9 :

deno upgrade
deno --version
# deno 2.9.0

deno desktop est marqué comme expérimental dans Deno 2.9. La surface de l'API se stabilise rapidement, mais des changements mineurs peuvent encore survenir entre les versions patch.

Étape 2 : Initialiser le projet

Créez le répertoire du projet et le fichier principal :

mkdir deno-notes-app
cd deno-notes-app

Créez deno.json (configuration de l'espace de travail Deno) :

{
  "name": "deno-notes",
  "version": "1.0.0",
  "tasks": {
    "dev": "deno desktop .",
    "build": "deno desktop build --all-targets ."
  },
  "permissions": {
    "read": true,
    "write": true,
    "net": ["localhost"]
  }
}

Étape 3 : Créer le point d'entrée bureau

Le point d'entrée est le fichier TypeScript qui s'exécute dans le processus Deno — pas dans le navigateur. C'est ici que vous contrôlez les fenêtres, les menus système et les fonctionnalités natives.

Créez main.ts :

// Point d'entrée bureau — s'exécute dans Deno, pas dans le WebView
 
const win = new Deno.BrowserWindow({
  title: "Deno Notes",
  width: 1200,
  height: 800,
  minWidth: 800,
  minHeight: 600,
});
 
// Servir l'interface utilisateur
const server = Deno.serve({ port: 8000 }, async (req) => {
  const url = new URL(req.url);
  const filePath = url.pathname === "/" ? "/index.html" : url.pathname;
 
  try {
    const file = await Deno.readFile(`./public${filePath}`);
    const ext = filePath.split(".").pop() ?? "txt";
    const types: Record<string, string> = {
      html: "text/html",
      css: "text/css",
      js: "application/javascript",
      json: "application/json",
    };
    return new Response(file, {
      headers: { "Content-Type": types[ext] ?? "text/plain" },
    });
  } catch {
    return new Response("Not found", { status: 404 });
  }
});
 
// Ouvrir le WebView sur le serveur local
win.loadURL("http://localhost:8000");
 
// Lier des fonctions Deno appelables depuis l'interface
win.bind("saveNote", async (id: string, content: string) => {
  const path = `./notes/${id}.md`;
  await Deno.mkdir("./notes", { recursive: true });
  await Deno.writeTextFile(path, content);
  return { ok: true };
});
 
win.bind("loadNote", async (id: string) => {
  try {
    const content = await Deno.readTextFile(`./notes/${id}.md`);
    return { ok: true, content };
  } catch {
    return { ok: false, content: "" };
  }
});
 
win.bind("listNotes", async () => {
  try {
    const entries = [];
    for await (const entry of Deno.readDir("./notes")) {
      if (entry.isFile && entry.name.endsWith(".md")) {
        entries.push(entry.name.replace(".md", ""));
      }
    }
    return { ok: true, notes: entries };
  } catch {
    return { ok: false, notes: [] };
  }
});
 
win.bind("deleteNote", async (id: string) => {
  await Deno.remove(`./notes/${id}.md`);
  return { ok: true };
});
 
// Gérer la fermeture de la fenêtre
win.addEventListener("close", () => {
  server.shutdown();
  Deno.exit(0);
});

Deno.BrowserWindow utilise par défaut le WebView natif du système (WKWebView sur macOS, WebView2 sur Windows, WebKitGTK sur Linux), ce qui maintient les binaires légers. Ajoutez { backend: "chromium" } dans les options si vous avez besoin d'un rendu identique sur toutes les plateformes.

Étape 4 : Créer l'interface web

Créez le répertoire public/ et le fichier public/index.html :

mkdir public

Créez public/index.html :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Deno Notes</title>
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <aside id="sidebar">
    <div class="sidebar-header">
      <h2>Notes</h2>
      <button id="newNote">+ Nouvelle</button>
    </div>
    <ul id="noteList"></ul>
  </aside>
 
  <main id="editor">
    <input id="noteTitle" type="text" placeholder="Titre de la note..." />
    <textarea id="noteContent" placeholder="Écrivez en Markdown..."></textarea>
    <div class="toolbar">
      <button id="saveBtn">Enregistrer</button>
      <button id="deleteBtn">Supprimer</button>
    </div>
  </main>
 
  <script src="/app.js"></script>
</body>
</html>

Créez public/styles.css :

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
 
body {
  font-family: system-ui, sans-serif;
  display: flex;
  height: 100vh;
  background: #1a1a2e;
  color: #e0e0e0;
}
 
#sidebar {
  width: 260px;
  background: #16213e;
  border-right: 1px solid #0f3460;
  display: flex;
  flex-direction: column;
}
 
.sidebar-header {
  padding: 1rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid #0f3460;
}
 
#newNote {
  background: #533483;
  color: white;
  border: none;
  padding: 0.4rem 0.8rem;
  border-radius: 6px;
  cursor: pointer;
}
 
#noteList { list-style: none; overflow-y: auto; flex: 1; }
#noteList li {
  padding: 0.75rem 1rem;
  cursor: pointer;
  border-bottom: 1px solid #0f3460;
  font-size: 0.9rem;
}
#noteList li:hover, #noteList li.active { background: #0f3460; }
 
#editor {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 1rem;
  gap: 0.75rem;
}
 
#noteTitle {
  font-size: 1.4rem;
  font-weight: 600;
  background: transparent;
  border: none;
  border-bottom: 2px solid #533483;
  color: #e0e0e0;
  padding: 0.5rem 0;
  outline: none;
}
 
#noteContent {
  flex: 1;
  background: #16213e;
  color: #e0e0e0;
  border: 1px solid #0f3460;
  border-radius: 8px;
  padding: 1rem;
  font-family: "Fira Code", monospace;
  font-size: 0.95rem;
  resize: none;
  outline: none;
}
 
.toolbar { display: flex; gap: 0.5rem; }
.toolbar button {
  padding: 0.5rem 1.5rem;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  font-weight: 600;
}
#saveBtn { background: #533483; color: white; }
#deleteBtn { background: #c0392b; color: white; }

Créez public/app.js :

let currentNote = null;
 
async function loadNoteList() {
  const result = await bindings.listNotes();
  const ul = document.getElementById("noteList");
  ul.innerHTML = "";
  for (const id of result.notes) {
    const li = document.createElement("li");
    li.textContent = id;
    li.dataset.id = id;
    if (id === currentNote) li.classList.add("active");
    li.addEventListener("click", () => openNote(id));
    ul.appendChild(li);
  }
}
 
async function openNote(id) {
  currentNote = id;
  const result = await bindings.loadNote(id);
  document.getElementById("noteTitle").value = id;
  document.getElementById("noteContent").value = result.content;
  document.querySelectorAll("#noteList li").forEach((li) => {
    li.classList.toggle("active", li.dataset.id === id);
  });
}
 
document.getElementById("newNote").addEventListener("click", () => {
  const id = "note-" + Date.now();
  currentNote = id;
  document.getElementById("noteTitle").value = id;
  document.getElementById("noteContent").value = "";
});
 
document.getElementById("saveBtn").addEventListener("click", async () => {
  const id = document.getElementById("noteTitle").value.trim();
  const content = document.getElementById("noteContent").value;
  if (!id) return alert("Veuillez donner un titre à la note.");
  currentNote = id;
  await bindings.saveNote(id, content);
  await loadNoteList();
});
 
document.getElementById("deleteBtn").addEventListener("click", async () => {
  if (!currentNote) return;
  await bindings.deleteNote(currentNote);
  currentNote = null;
  document.getElementById("noteTitle").value = "";
  document.getElementById("noteContent").value = "";
  await loadNoteList();
});
 
loadNoteList();

La variable globale bindings est injectée automatiquement par Deno.BrowserWindow. Chaque fonction enregistrée avec win.bind() dans le point d'entrée devient disponible sous bindings.nomDeLaFonction() dans le WebView. Tous les appels retournent des Promises.

Étape 5 : Ajouter une barre de menus native

Ajoutez ce code à main.ts après les appels win.bind() :

win.setMenu({
  items: [
    {
      label: "Fichier",
      submenu: [
        { id: "new", label: "Nouvelle note", accelerator: "CmdOrCtrl+N" },
        { id: "save", label: "Enregistrer", accelerator: "CmdOrCtrl+S" },
        { type: "separator" },
        { id: "quit", label: "Quitter", accelerator: "CmdOrCtrl+Q" },
      ],
    },
    {
      label: "Édition",
      submenu: [
        { id: "undo", label: "Annuler", role: "undo" },
        { id: "redo", label: "Rétablir", role: "redo" },
        { type: "separator" },
        { id: "cut", label: "Couper", role: "cut" },
        { id: "copy", label: "Copier", role: "copy" },
        { id: "paste", label: "Coller", role: "paste" },
      ],
    },
  ],
});
 
win.addEventListener("menuclick", (e) => {
  const id = e.detail.id;
  if (id === "new") win.executeJs("document.getElementById('newNote').click()");
  if (id === "save") win.executeJs("document.getElementById('saveBtn').click()");
  if (id === "quit") Deno.exit(0);
});

Les éléments avec role comme "cut", "copy" ou "paste" délèguent au presse-papiers natif du système d'exploitation — aucun JavaScript supplémentaire n'est nécessaire.

Étape 6 : Ajouter une icône de barre des tâches

Ajoutez une icône dans la zone de notification pour un accès rapide :

const iconPath = new URL("./assets/icon.png", import.meta.url).pathname;
const iconBytes = await Deno.readFile(iconPath);
 
const tray = new Deno.Tray();
tray.setIcon(iconBytes);
tray.setTooltip("Deno Notes");
 
tray.setContextMenu({
  items: [
    { id: "show", label: "Afficher la fenêtre" },
    { id: "quit", label: "Quitter" },
  ],
});
 
tray.addEventListener("contextmenuclick", (e) => {
  if (e.detail.id === "show") {
    win.show();
    win.focus();
  }
  if (e.detail.id === "quit") Deno.exit(0);
});

Créez le dossier assets/ et placez-y une image PNG de 32x32 pixels.

Étape 7 : Lancer en mode développement

deno task dev
# Équivalent à : deno desktop .

Deno détecte automatiquement le point d'entrée depuis deno.json et ouvre la fenêtre. Le rechargement à chaud n'est pas encore intégré — relancez deno task dev après avoir modifié main.ts, mais les modifications dans public/ sont prises en compte dès le rafraîchissement du WebView.

En développement, ouvrez les DevTools dans le WebView avec win.openDevTools() dans votre point d'entrée, ou en ajoutant l'option --inspect à la commande deno desktop.

Étape 8 : Compiler et empaqueter pour la distribution

Compilez un binaire natif pour la plateforme courante :

deno desktop build .
# Résultats : ./dist/deno-notes.app    (macOS)
#              ./dist/deno-notes.exe    (Windows)
#              ./dist/deno-notes.AppImage (Linux)

Compilation croisée pour toutes les plateformes en une seule commande :

deno desktop build --all-targets .

Formats spécifiques :

# Image disque macOS
deno desktop build --output dist/DenoNotes.dmg .
 
# Installeur Windows
deno desktop build --output dist/DenoNotes.msi .
 
# Paquet Debian
deno desktop build --output dist/deno-notes.deb .

La compilation croisée fonctionne depuis n'importe quelle machine — vous n'avez pas besoin d'un Mac pour générer le fichier .dmg. Deno inclut automatiquement la chaîne d'outils de la plateforme cible.

Structure finale du projet

deno-notes-app/
├── deno.json
├── main.ts          # Point d'entrée bureau (processus Deno)
├── assets/
│   └── icon.png
├── notes/           # Créé au moment de l'exécution
└── public/
    ├── index.html
    ├── styles.css
    └── app.js

Comment fonctionne le pont

Le modèle de communication entre Deno et le WebView :

┌─────────────────────────────────────────┐
│  Processus Deno (main.ts)                │
│                                          │
│  Deno.BrowserWindow                      │
│  ├─ win.bind("saveNote", fn)  ◄──────────┼──── enregistre un handler natif
│  ├─ win.executeJs("...")      ──────────►│     appelle du JS dans WebView
│  └─ win.addEventListener(...)            │
│                                          │
└──────────────────────┬───────────────────┘
                       │  IPC (sécurisé)
┌──────────────────────▼───────────────────┐
│  WebView (public/app.js)                  │
│                                          │
│  bindings.saveNote(id, content)  ────────┼──► appelle le handler Deno
│  bindings.loadNote(id)           ────────┘
│                                          │
└──────────────────────────────────────────┘

Le canal IPC est cloisonné — le WebView ne peut pas accéder directement au système de fichiers. Tous les accès doivent passer par des fonctions liées dans le processus Deno, ce qui vous donne une frontière de sécurité nette.

Comparaison avec Electron et Tauri

FonctionnalitéDeno DesktopElectronTauri
LangageTypeScriptJavaScriptRust + JS
Taille du binaire~15 Mo80–150 Mo5–10 Mo
BackendDeno (V8)Node.js (V8)Rust
WebView natifOui (par défaut)Non (Chromium)Oui
Chromium intégréOptionnelToujoursNon
Compilation croiséeIntégréeVia electron-builderVia Tauri CLI
Compatibilité NPMComplète (compat Node)ComplèteVia pont JS

Résolution des problèmes

La fenêtre s'ouvre mais affiche un écran blanc Assurez-vous que le serveur démarre avant l'appel à win.loadURL(). Ajoutez un petit délai ou attendez l'événement ready du serveur.

bindings n'est pas défini dans le WebView Vérifiez que win.bind() est appelé avant win.loadURL(). La liaison est injectée dans la page au chargement.

La compilation échoue avec l'erreur DENO_DESKTOP_UNSTABLE La commande deno desktop nécessite le drapeau --unstable-desktop pour les versions de Deno antérieures à 2.9.0.

L'icône de barre des tâches n'apparaît pas sur Linux Vous devez installer libayatana-appindicator3-1 : sudo apt install libayatana-appindicator3-1.

Pour aller plus loin

Maintenant que votre application bureau fonctionne :

  • Ajoutez un aperçu Markdown : Rendez le Markdown en HTML avec la bibliothèque Marked chargée depuis un CDN
  • Utilisez Fresh ou Astro : Remplacez Deno.serve() manuel par un framework complet — deno desktop les détecte automatiquement
  • Ajoutez une authentification : Utilisez Deno.env dans votre point d'entrée pour lire des secrets sans les exposer au WebView
  • Publiez dans les stores : Empaquetez en .pkg (Mac App Store) ou .msix (Microsoft Store) avec signature de code
  • Explorez Deno Windowing : Visitez windowing.deno.dev pour l'API de fenêtrage étendue avec support multi-fenêtres

Conclusion

La commande deno desktop de Deno 2.9 abaisse considérablement la barrière au développement d'applications bureau multiplateformes. Vous bénéficiez d'un backend TypeScript natif, de toute la plateforme web pour votre interface, d'une frontière de sécurité propre via window.bind(), et d'une compilation croisée sans configuration — le tout sans les 150 Mo de surcharge d'Electron. L'API est encore expérimentale et en évolution, mais ses fondations sont déjà suffisamment solides pour livrer de vrais produits.

Pour les dernières informations sur la surface de l'API, consultez la documentation officielle de Deno Desktop.