The Deno Runtime Environment
What is Deno?
Deno is a modern JavaScript/TypeScript runtime created by Ryan Dahl (also the creator of Node.js). It was designed to address shortcomings in Node.js while embracing modern web standards.
Key Differences from Node.js
TypeScript Support
// Node.js: Requires compilation
// package.json, tsconfig.json, tsc or ts-node
// Deno: Native TypeScript
// Just run: deno run script.ts
function greet(name: string): string {
return `Hello, ${name}!`
}
Module System
// Node.js: npm packages
import express from 'express'
import _ from 'lodash'
// Deno: URL imports
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"
// Or using npm: prefix
import _ from "npm:lodash@4.17.21"
Security Model
# Node.js: Full system access by default
node script.js
# Deno: Explicit permissions required
deno run --allow-net --allow-read script.ts
Web Standards in Deno
Deno implements web platform APIs, making code portable between browser and server:
Fetch API
// Same as browser fetch
const response = await fetch('https://api.example.com/data')
const data = await response.json()
Request/Response
// Standard web Request object
serve((req: Request) => {
const url = new URL(req.url)
const method = req.method
const headers = req.headers
// Standard web Response object
return new Response('Hello', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
})
Headers, URL, FormData
// All standard web APIs
const headers = new Headers()
headers.set('Authorization', 'Bearer token')
const url = new URL('https://example.com/path?query=value')
console.log(url.searchParams.get('query')) // 'value'
const formData = new FormData()
formData.append('file', blob)
URL Imports
Deno imports modules directly from URLs:
Standard Library
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { join } from "https://deno.land/std@0.168.0/path/mod.ts"
import { crypto } from "https://deno.land/std@0.168.0/crypto/mod.ts"
Third-Party Modules
// Deno-specific modules
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"
import { parse } from "https://deno.land/x/xml@2.1.1/mod.ts"
// npm packages
import Stripe from "npm:stripe@13.0.0"
import _ from "npm:lodash@4.17.21"
esm.sh for npm packages
// esm.sh converts npm packages to ES modules
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
import dayjs from "https://esm.sh/dayjs@1.11.10"
Version Pinning
Always pin versions for reproducibility:
// Good: Pinned version
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
// Bad: Latest (can break)
import { serve } from "https://deno.land/std/http/server.ts"
The Deno Permission Model
Deno runs with no permissions by default. For Edge Functions, Supabase configures necessary permissions.
Permission Types
| Permission | Flag | Allows |
|---|---|---|
| Network | --allow-net | HTTP requests |
| Read | --allow-read | File system read |
| Write | --allow-write | File system write |
| Env | --allow-env | Environment variables |
| Run | --allow-run | Subprocess execution |
In Edge Functions
Supabase Edge Functions run with:
--allow-net: For HTTP and database connections--allow-env: For environment variables
No file system access (functions are stateless).
Async/Await in Deno
Deno supports top-level await:
// Top-level await (no async wrapper needed)
const response = await fetch('https://api.example.com')
const data = await response.json()
console.log(data)
Async Request Handlers
serve(async (req: Request) => {
// Async operations work naturally
const { data } = await supabase.from('posts').select('*')
const enriched = await enrichPosts(data)
return new Response(JSON.stringify(enriched))
})
Working with JSON
Parsing Request Body
serve(async (req: Request) => {
// Parse JSON body
const body = await req.json()
// Validate
if (!body.email || !body.message) {
return new Response(
JSON.stringify({ error: 'Missing required fields' }),
{ status: 400 }
)
}
// Process...
})
Returning JSON
serve(async (req: Request) => {
const result = {
success: true,
data: { id: 123, name: 'Example' }
}
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
})
})
Common Patterns in Edge Functions
CORS Headers
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req: Request) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// Your logic...
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
})
Request Method Routing
serve(async (req: Request) => {
const { method } = req
switch (method) {
case 'GET':
return handleGet(req)
case 'POST':
return handlePost(req)
case 'PUT':
return handlePut(req)
case 'DELETE':
return handleDelete(req)
default:
return new Response('Method not allowed', { status: 405 })
}
})
URL Parameters
serve(async (req: Request) => {
const url = new URL(req.url)
// Query parameters
const page = url.searchParams.get('page') || '1'
const limit = url.searchParams.get('limit') || '10'
// Path parsing
const path = url.pathname
// /functions/v1/users/123 -> extract 123
return new Response(JSON.stringify({ page, limit }))
})
TypeScript Features
Type Safety
interface RequestBody {
email: string
name: string
metadata?: Record<string, unknown>
}
interface ResponseData {
success: boolean
userId?: string
error?: string
}
serve(async (req: Request): Promise<Response> => {
const body: RequestBody = await req.json()
const response: ResponseData = {
success: true,
userId: '123'
}
return new Response(JSON.stringify(response))
})
Input Validation with Zod
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"
const RequestSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional()
})
serve(async (req: Request) => {
try {
const body = await req.json()
const validated = RequestSchema.parse(body)
// `validated` is typed and validated
console.log(validated.email) // TypeScript knows this is string
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(
JSON.stringify({ errors: error.errors }),
{ status: 400 }
)
}
throw error
}
})
Debugging and Logging
Console Logging
serve(async (req: Request) => {
console.log('Request received:', req.method, req.url)
try {
const body = await req.json()
console.log('Body:', JSON.stringify(body))
// Process...
console.log('Processing complete')
} catch (error) {
console.error('Error:', error.message)
throw error
}
})
Logs appear in Supabase Dashboard under Edge Functions > Logs.
Key Takeaways
- Web Standards: fetch, Request, Response work natively
- TypeScript Native: No compilation needed
- URL Imports: Import from URLs, pin versions
- Secure by Default: Explicit permissions (handled by Supabase)
- Modern JavaScript: Top-level await, ES modules
- Type Safety: Full TypeScript support
Next Steps
With the runtime understood, we'll explore common use cases for Edge Functions in real applications.
Deno isn't just "Node.js 2.0"—it's a rethinking of server-side JavaScript with web standards at its core. If you know modern JavaScript, you already know most of Deno.

