Introduction
Deno Deploy is Deno's serverless edge computing platform, designed to simplify global deployments without any server setup. In 2026, it stands out for its blazing speed (cold starts under 1ms), seamless integration with the secure Deno runtime, and a free tier for small projects. Unlike Vercel or Cloudflare Workers, Deno Deploy runs TypeScript/Deno directly without bundling and supports KV, D1, and queues.
Why use it? For scalable REST APIs distributed across 40+ global edge locations with no upfront costs. Picture an API responding in 10ms from Paris or Tokyo. This beginner-friendly tutorial takes you from the basics (simple HTTP server) to advanced features (routes with persistent KV), using fully functional code. By the end, your app will be live in 10 minutes—bookmark-worthy for any Deno developer.
Prerequisites
- Deno 2.0+ installed (run
deno --versionto check) - Free account on deno.com and a Deploy project created (dashboard.deno.com > Deployments > New Project)
- API Token: dashboard > Settings > API Tokens > Create Token (name it "deploy-token")
- Git installed (optional, for CI/CD)
- Editor like VS Code with the Deno extension
Install deployctl
curl -fsSL https://deno.land/x/install/install.sh | sh
# Restart the terminal or source the profile
# Install deployctl (official Deno Deploy CLI)
denop install -A -f --unstable https://deno.land/x/deploy@0.8.6/deployctl.ts
# Verify
deployctl --versionThis script installs Deno if it's missing, then deployctl for managing deployments. The --unstable flag is required for current edge features. Stick to the pure Deno ecosystem and avoid global npm versions; always verify with --version.
Set Up Your Local Project
Create an empty folder called mon-app-deno and navigate into it. Copy the following code snippets. Deno Deploy runs directly from a .ts file without building, but a deno.json config optimizes imports and locks versions for reproducibility.
Configure deno.json
{
"name": "mon-app-deno-deploy",
"version": "0.1.0",
"lock": true,
"tasks": {
"start": "deno run --allow-net --allow-env main.ts",
"deploy": "deployctl deploy --project=monprojet main.ts"
},
"imports": {
"$std/http/": "https://deno.land/std@0.224.0/http/"
}
}This file locks std dependencies to prevent version breaks on Deploy. The tasks make it easy to run deno task start locally or deploy. Replace monprojet with your dashboard project name; --lock true ensures identical deployments.
Basic HTTP Server
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
serve((req: Request) => {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response(JSON.stringify({ message: "Hello Deno Deploy!", timestamp: new Date().toISOString() }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
}, { port: 8000 });
console.log("Serveur local sur http://localhost:8000");This minimal handler uses std's serve for an HTTP server. It returns JSON on / with a live timestamp. Test locally with deno task start (requires --allow-net). Common pitfall: forgetting JSON headers causes fetch clients to fail; port 8000 avoids conflicts.
Test Locally
Run deno task start (or deno run --allow-net main.ts). Open http://localhost:8000. You'll see {"message":"Hello Deno Deploy!","timestamp":"2026-..."}. Stop with Ctrl+C. This step validates your code before deployment, saving costly iterations.
Add API Routes
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
serve((req: Request) => {
const url = new URL(req.url);
const pathname = url.pathname;
if (pathname === "/api/users") {
if (req.method === "GET") {
return new Response(JSON.stringify([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
]), {
headers: { "Content-Type": "application/json" },
});
}
if (req.method === "POST") {
const body = await req.json();
return new Response(JSON.stringify({ message: "User créé", user: body }), {
status: 201,
headers: { "Content-Type": "application/json" },
});
}
}
if (pathname === "/") {
return new Response(JSON.stringify({ message: "API prête !" }), {
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
});Adds /api/users GET (mock list) and POST (echoes body). Uses req.json() parsing for inputs. Think of it like Express but native to Deno with zero dependencies. Pitfall: strictly handle req.method; skipping await on POST leads to empty body crashes.
Integrate KV for Persistence
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
// KV on Deno Deploy (auto-created or via dashboard)
const kv = await Deno.openKv();
serve(async (req: Request) => {
const url = new URL(req.url);
const pathname = url.pathname;
if (pathname === "/api/users" && req.method === "GET") {
const users: Record<string, any>[] = [];
for await (const entry of kv.list({ prefix: ["users"] })) {
users.push(entry.value);
}
return new Response(JSON.stringify(users), {
headers: { "Content-Type": "application/json" },
});
}
if (pathname === "/api/users" && req.method === "POST") {
const body = await req.json();
const id = crypto.randomUUID();
await kv.set(["users", id], { id, ...body });
return new Response(JSON.stringify({ message: "User créé", id }), {
status: 201,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ error: "Not Found" }), { status: 404 });
});Deno KV provides global, persistent, edge-native storage for users. Deno.openKv() works automatically on Deploy. GET iterates and lists; POST generates UUID and sets. Like Redis but serverless and free. Pitfall: handler must be async for KV; skipping await kv.set loses data.
Deploy to Deno Deploy
# Set env vars (add to .env or shell)
export DENO_DEPLOY_TOKEN="your_dashboard_token"
export DENO_PROJECT_NAME="monprojet"
# Dry-run to validate
deployctl deploy --project=$DENO_PROJECT_NAME main.ts --dry-run
# Deploy (excludes node_modules, .git)
deployctl deploy --project=$DENO_PROJECT_NAME main.ts --exclude="*.md,node_modules"
# Live URL: https://monprojet.deno.dev
# Logs
deployctl logs --project=$DENO_PROJECT_NAME --watchdeployctl pushes to the edge in seconds. Use --dry-run to validate without deploying. Keep the token secure in env vars. Auto URL: projet.deno.dev. Pitfall: expired token causes 401; --exclude skips unnecessary files, shrinking bundle size by 90%.
Best Practices
- Locks and versions: Always use
deno.jsonwithlock: truefor stable deployments. - Minimal permissions: Local uses
--allow-net --allow-env; Deploy handles it automatically. - CORS headers: Add
Access-Control-Allow-Origin: *for frontend APIs. - Monitoring: Use
deployctl logs --watchand dashboard metrics. - CI/CD: Integrate GitHub Actions with
deployctlfor automated deploys.
Common Errors to Avoid
- Invalid token: 401 on deploy → recreate with Deploy scopes.
- Broken imports: Changing std versions → lock with deno.json.
- Slow cold starts: Avoid heavy deps; std is edge-optimized.
- Non-async KV: Forgetting
await→ data not persisted, sync handler crashes.
Next Steps
Master advanced Deno Deploy: queues, D1 SQL, custom domains. Check the official Deno Deploy docs. For expert training, explore our Learni courses on Deno and serverless. Next level: full-stack APIs with Fresh + Deploy.