15  Deno Modules and Dependencies

Great to meet you! Let me explain Deno’s module system, which is quite different from Node.js but cleaner in many ways.

15.1 How Deno Handles Modules

Unlike Node.js with node_modules and package.json, Deno imports modules directly from URLs or local file paths. No package manager needed!

┌─────────────────────────────────────────────────────┐
│  Deno Module System                                 │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Your Code                                          │
│  ┌──────────────────────────────────────┐           │
│  │ import { serve } from "..."          │           │
│  └────────────┬─────────────────────────┘           │
│               │                                     │
│               ├──→ URL imports (remote)             │
│               │    https://deno.land/std@0.224.0/   │
│               │                                     │
│               ├──→ File imports (local)             │
│               │    ./utils.ts                       │
│               │    ../lib/helpers.ts                │
│               │                                     │
│               └──→ npm: specifier (Node packages)   │
│                    npm:express                      │
│                                                     │
└─────────────────────────────────────────────────────┘

15.2 Three Ways to Import

1. URL Imports (Deno’s original way)

import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";

2. Import Maps (cleaner aliases)

First, create deno.json:

{
  "imports": {
    "std/": "https://deno.land/std@0.224.0/",
    "oak": "https://deno.land/x/oak@v12.6.1/mod.ts"
  }
}

Then use short aliases:

import { serve } from "std/http/server.ts";
import { Application } from "oak";

3. npm Packages (for Node.js compatibility)

import express from "npm:express@4.18.2";
import lodash from "npm:lodash@4.17.21";

15.3 How Dependencies are Cached

When you run your code, Deno downloads and caches modules:

First Run:
┌──────────────┐
│  main.ts     │──→ import from URL
└──────────────┘
       ↓
   Download
       ↓
┌──────────────────────────────────┐
│  DENO_DIR Cache                  │
│  ~/Library/Caches/deno/ (macOS)  │
│  ├─ deps/                        │
│  └─ npm/                         │
└──────────────────────────────────┘

Subsequent Runs:
┌──────────────┐
│  main.ts     │──→ Use cached version (instant!)
└──────────────┘

To reload: deno run --reload main.ts

15.4 Creating Barrel Exports (mod.ts pattern)

A “barrel” file re-exports multiple modules from a single entry point. In Deno, this is conventionally called mod.ts.

15.4.1 Project Structure Example

my_library/
├── mod.ts              ← Barrel export (main entry point)
├── deno.json
├── src/
│   ├── auth.ts
│   ├── database.ts
│   └── utils.ts
└── tests/
    └── auth_test.ts

15.4.2 Step-by-Step: Creating a Barrel Export

1. Create individual modules

src/auth.ts:

export interface User {
  id: string;
  name: string;
  email: string;
}

export function authenticate(token: string): User | null {
  // Authentication logic
  if (token === "valid") {
    return { id: "1", name: "John", email: "john@example.com" };
  }
  return null;
}

export class AuthError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "AuthError";
  }
}

src/database.ts:

export async function queryDB(sql: string): Promise<unknown[]> {
  // Database query logic
  console.log(`Executing: ${sql}`);
  return [];
}

export interface DBConfig {
  host: string;
  port: number;
  database: string;
}

src/utils.ts:

export function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

export function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

2. Create the barrel export file: mod.ts

// Re-export everything from auth
export { authenticate, AuthError } from "./src/auth.ts";
export type { User } from "./src/auth.ts";

// Re-export everything from database
export { queryDB } from "./src/database.ts";
export type { DBConfig } from "./src/database.ts";

// Re-export everything from utils
export { formatDate, validateEmail } from "./src/utils.ts";

// Alternative: export everything at once
// export * from "./src/auth.ts";
// export * from "./src/database.ts";
// export * from "./src/utils.ts";

3. Users can now import from the barrel

// Instead of multiple imports:
// import { authenticate } from "./my_library/src/auth.ts";
// import { queryDB } from "./my_library/src/database.ts";
// import { formatDate } from "./my_library/src/utils.ts";

// Single import from barrel:
import { 
  authenticate, 
  queryDB, 
  formatDate,
  type User,
  type DBConfig 
} from "./my_library/mod.ts";

const user = authenticate("valid");
const results = await queryDB("SELECT * FROM users");
const today = formatDate(new Date());

15.5 Barrel Export Patterns

Pattern 1: Selective Re-exports (Recommended)

// mod.ts - Only export public API
export { publicFunction } from "./src/internal.ts";
// privateFunction is NOT exported - stays internal

Pattern 2: Re-export Everything

// mod.ts - Export all
export * from "./src/auth.ts";
export * from "./src/database.ts";

Pattern 3: Rename Exports

// mod.ts - Rename for clarity
export { 
  authenticate as login,
  AuthError as LoginError 
} from "./src/auth.ts";

Pattern 4: Nested Barrels

library/
├── mod.ts                    ← Top-level barrel
├── auth/
│   ├── mod.ts               ← Auth barrel
│   ├── jwt.ts
│   └── oauth.ts
└── db/
    ├── mod.ts               ← DB barrel
    ├── postgres.ts
    └── sqlite.ts

auth/mod.ts:

export * from "./jwt.ts";
export * from "./oauth.ts";

mod.ts (top-level):

// Re-export sub-barrels
export * from "./auth/mod.ts";
export * from "./db/mod.ts";

15.6 Quick Comparison: Deno vs Node.js

Node.js:                           Deno:
─────────────────────────────────────────────────────
package.json + node_modules    →   Direct URL imports
npm install                    →   Auto-download on run
require() / import             →   ES modules only
index.js (convention)          →   mod.ts (convention)

15.7 Try It Yourself

Create this simple project:

mkdir my_math_lib
cd my_math_lib

mod.ts:

export { add, subtract } from "./src/basic.ts";
export { multiply, divide } from "./src/advanced.ts";
export type { MathOperation } from "./src/types.ts";

src/types.ts:

export type MathOperation = "add" | "subtract" | "multiply" | "divide";

src/basic.ts:

export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

src/advanced.ts:

export function multiply(a: number, b: number): number {
  return a * b;
}

export function divide(a: number, b: number): number {
  if (b === 0) throw new Error("Division by zero");
  return a / b;
}

example.ts (usage):

import { add, multiply, type MathOperation } from "./my_math_lib/mod.ts";

console.log(add(5, 3));        // 8
console.log(multiply(5, 3));   // 15

const op: MathOperation = "add";

Run it:

deno run example.ts

15.8 Key Takeaways

  1. No node_modules: Dependencies are cached globally
  2. URL-based imports: Direct or via import maps
  3. mod.ts convention: Main entry point (like Python’s __init__.py)
  4. Barrel exports: Clean API, hide internal details
  5. Type exports: Use export type for TypeScript types