Skip to main content
Extensions are the building blocks of Deno’s runtime. They provide native functionality to JavaScript by exposing Rust operations (ops) and bundling JavaScript code.

What are Extensions?

Extensions bundle together:
  • Ops - Rust functions callable from JavaScript
  • JavaScript/TypeScript code - High-level APIs
  • Dependencies - Other extensions required
  • State - Extension-specific initialization
Location: All extensions live in the ext/ directory.
ext/
├── fs/                # File system operations
├── net/               # Networking
├── fetch/             # Fetch API
├── web/               # Web APIs
├── crypto/            # Cryptography
└── ...

Extension Structure

A typical extension contains:
ext/my_extension/
├── Cargo.toml         # Crate dependencies
├── lib.rs             # Extension definition
├── ops.rs             # Op implementations (optional)
├── 01_my_api.js       # JavaScript code
└── README.md          # Documentation

Creating a New Extension

Step 1: Create the Directory

mkdir ext/my_extension
cd ext/my_extension

Step 2: Create Cargo.toml

[package]
name = "deno_my_extension"
version = "0.1.0"
edition = "2021"
license = "MIT"

[dependencies]
deno_core.workspace = true
serde = { workspace = true, features = ["derive"] }

# Add other dependencies as needed

Step 3: Define the Extension

Create lib.rs:
// ext/my_extension/lib.rs
use deno_core::extension;

// Define ops
#[op2(fast)]
fn op_my_add(a: i32, b: i32) -> i32 {
  a + b
}

#[op2]
#[string]
fn op_my_greet(#[string] name: String) -> String {
  format!("Hello, {}!", name)
}

// Register extension
extension!(
  deno_my_extension,
  ops = [
    op_my_add,
    op_my_greet,
  ],
  esm = [
    "01_my_extension.js",
  ],
);

Step 4: Create JavaScript API

Create 01_my_extension.js:
const core = globalThis.Deno.core;
const ops = core.ops;

function add(a, b) {
  return ops.op_my_add(a, b);
}

function greet(name) {
  return ops.op_my_greet(name);
}

// Export to globalThis.Deno namespace
globalThis.Deno = globalThis.Deno || {};
globalThis.Deno.MyExtension = {
  add,
  greet,
};

Step 5: Register in Runtime

Add to runtime/worker.rs:
let extensions = vec![
  deno_web::deno_web::init_ops_and_esm::<Permissions>(...),
  deno_fetch::deno_fetch::init_ops_and_esm::<Permissions>(...),
  deno_my_extension::deno_my_extension::init_ops_and_esm(),  // Add this
  // ...
];
Add to workspace Cargo.toml:
[workspace]
members = [
  "ext/my_extension",
  # ...
]

[workspace.dependencies]
deno_my_extension = { version = "0.1.0", path = "./ext/my_extension" }

Step 6: Add to CLI Dependencies

In cli/Cargo.toml:
[dependencies]
deno_my_extension.workspace = true

Writing Ops

Ops are the interface between Rust and JavaScript.

Op Types

#[op2(fast)]
fn op_add(a: i32, b: i32) -> i32 {
  a + b
}
Fast synchronous operations with no async overhead.

Op Parameters

Common parameter types:
// Strings
fn op_example(#[string] s: String) -> String { s }

// Buffers
fn op_example(#[buffer] buf: &[u8]) -> usize { buf.len() }

// Serde (JSON-like)
fn op_example(#[serde] obj: MyStruct) -> MyStruct { obj }

// Small integers (optimized)
fn op_example(#[smi] n: i32) -> i32 { n }

// BigInt
fn op_example(#[bigint] n: i64) -> i64 { n }

// State
fn op_example(state: &mut OpState) -> u32 { 42 }

Op Return Types

// Direct return
#[op2(fast)]
fn op_add(a: i32, b: i32) -> i32 { a + b }

// String return
#[op2]
#[string]
fn op_greet() -> String { "Hello".to_string() }

// Buffer return
#[op2]
#[buffer]
fn op_bytes() -> Vec<u8> { vec![1, 2, 3] }

// Serde return
#[op2]
#[serde]
fn op_data() -> MyStruct { MyStruct { /* ... */ } }

// Result return
#[op2]
fn op_fallible() -> Result<i32, AnyError> {
  Ok(42)
}

Resources

Resources represent system handles like files and sockets.

Defining a Resource

use deno_core::Resource;
use std::rc::Rc;

struct MyResource {
  data: AsyncRefCell<Vec<u8>>,
}

impl Resource for MyResource {
  fn name(&self) -> Cow<str> {
    "myResource".into()
  }
  
  fn close(self: Rc<Self>) {
    // Cleanup logic
  }
}

Creating Resources

#[op2]
fn op_create_resource(
  state: &mut OpState,
) -> u32 {  // Returns resource ID (rid)
  let resource = MyResource {
    data: AsyncRefCell::new(vec![]),
  };
  
  state.resource_table.add(resource)
}

Using Resources

#[op2(async)]
async fn op_read_resource(
  state: Rc<RefCell<OpState>>,
  #[smi] rid: u32,
) -> Result<Vec<u8>, AnyError> {
  let resource = state.borrow()
    .resource_table
    .get::<MyResource>(rid)?;
  
  let data = resource.data.borrow().await;
  Ok(data.clone())
}

Closing Resources

#[op2]
fn op_close_resource(
  state: &mut OpState,
  #[smi] rid: u32,
) -> Result<(), AnyError> {
  state.resource_table.close(rid)?;
  Ok(())
}

Example: File System Extension

Let’s look at a simplified version of the fs extension:
use deno_core::extension;
use std::path::Path;

#[op2]
#[string]
fn op_fs_cwd() -> Result<String, std::io::Error> {
  std::env::current_dir()
    .map(|p| p.to_string_lossy().to_string())
}

#[op2(async)]
async fn op_fs_read_file(
  state: Rc<RefCell<OpState>>,
  #[string] path: String,
) -> Result<Vec<u8>, std::io::Error> {
  // Check permissions
  {
    let mut state = state.borrow_mut();
    let permissions = state.borrow_mut::<PermissionsContainer>();
    permissions.check_read(Path::new(&path))?;
  }
  
  // Read file
  tokio::fs::read(path).await
}

extension!(
  deno_fs,
  deps = [ deno_web ],
  ops = [
    op_fs_cwd,
    op_fs_read_file,
  ],
  esm = [ "30_fs.js" ],
);

Existing Extensions Reference

ext/fs

File system operations: read, write, stat, etc.

ext/net

Networking: TCP, UDP, TLS connections

ext/fetch

Fetch API implementation

ext/web

Web APIs: TextEncoder, URL, Events, etc.

ext/crypto

Web Crypto API

ext/http

HTTP server implementation

Best Practices

1

Always check permissions

Never perform privileged operations without checking permissions first.
let permissions = state.borrow_mut::<PermissionsContainer>();
permissions.check_read(&path)?;
2

Use appropriate op types

  • Use #[op2(fast)] for hot paths with simple types
  • Use #[op2(async)] for I/O operations
  • Use regular #[op2] for everything else
3

Handle errors properly

Return Result<T, AnyError> and use ? operator:
fn op_example(path: String) -> Result<Vec<u8>, AnyError> {
  let data = std::fs::read(path)?;
  Ok(data)
}
4

Document your APIs

Add JSDoc comments to JavaScript wrappers:
/**
 * Reads a file from the file system.
 * @param {string} path - Path to the file
 * @returns {Promise<Uint8Array>} File contents
 */
async function readFile(path) {
  return await ops.op_fs_read_file(path);
}
5

Test thoroughly

Add tests in tests/unit/ and spec tests in tests/specs/.

Testing Extensions

Unit Tests

Add Rust unit tests:
#[cfg(test)]
mod tests {
  use super::*;
  
  #[test]
  fn test_op_add() {
    assert_eq!(op_my_add(2, 3), 5);
  }
}

Integration Tests

Create spec tests in tests/specs/my_extension/:
// tests/specs/my_extension/__test__.jsonc
{
  "tests": {
    "basic_usage": {
      "args": "run main.ts",
      "output": "main.out"
    }
  }
}
// tests/specs/my_extension/main.ts
const result = Deno.MyExtension.add(2, 3);
console.log(result);  // 5

Common Patterns

#[op2(async)]
async fn op_sleep(
  state: Rc<RefCell<OpState>>,
  millis: u64,
) -> Result<(), AnyError> {
  let cancel = state.borrow().borrow::<CancelHandle>().clone();
  
  tokio::select! {
    _ = tokio::time::sleep(Duration::from_millis(millis)) => Ok(()),
    _ = cancel.cancelled() => Err(anyhow!("Cancelled")),
  }
}
#[op2]
#[serde]
fn op_batch_process(
  #[serde] items: Vec<String>,
) -> Result<Vec<String>, AnyError> {
  items.iter()
    .map(|item| process_item(item))
    .collect()
}
#[derive(Debug, thiserror::Error)]
pub enum MyError {
  #[error("{0}")]
  Custom(String),
  #[error("IO error: {0}")]
  Io(#[from] std::io::Error),
}

#[op2]
fn op_fallible() -> Result<(), MyError> {
  Err(MyError::Custom("Something went wrong".into()))
}

Debugging Extensions

See Debugging for detailed debugging techniques. Quick tips:
// Use eprintln! for debug output
eprintln!("Debug: {:?}", value);

// Use dbg! macro
dbg!(some_variable);

// Enable logging
log::debug!("Operation completed: {:?}", result);

Next Steps

Testing

Learn how to test your extensions

Debugging

Debug extension issues

Build docs developers (and LLMs) love