# Deployment and DevOps Standards for Deno
This document outlines the deployment and DevOps standards for Deno projects, aiming to provide a consistent and reliable approach for building, testing, and deploying Deno applications. These standards focus on leveraging Deno's built-in features, modern tooling, and best practices for maintainability, performance, and security.
## 1. Build Processes and CI/CD
### 1.1. Standard: Use "deno task" for Build Automation
**Do This:** Define build, test, and lint tasks within your "deno.json" or "deno.jsonc" file. This centralizes your project's automation commands, making them easy to discover and execute.
**Don't Do This:** Rely on ad-hoc shell scripts or duplicated commands across different environments.
**Why:** "deno task" provides a standardized, repeatable, and self-documenting way to execute common project tasks. It helps avoid environment-specific inconsistencies and simplifies CI/CD configuration.
**Example:**
"""jsonc
// deno.jsonc
{
"tasks": {
"start": "deno run --allow-net --allow-read main.ts",
"build": "deno bundle --no-check main.ts dist/bundle.js",
"test": "deno test --allow-read --allow-net",
"lint": "deno lint",
"fmt": "deno fmt"
},
"fmt": {
"files": [
"main.ts",
"src/",
"test/"
],
"options": {
"useTabs": false,
"lineWidth": 120,
"indentWidth": 2,
"singleQuote": true,
"proseWrap": "always"
}
},
"lint": {
"files": [
"main.ts",
"src/",
"test/"
],
"rules": {
"tags": [
"recommended"
]
}
}
}
"""
**Explanation:** This "deno.jsonc" file defines tasks for starting the server, building the application, running tests, linting, and formatting. The "lint" and "fmt" configurations are also directly embedded within the file.
**Anti-Pattern:** Using "package.json" scripts (from Node.js) for Deno projects. While possible through compatibility layers, it misses the point of Deno's integrated tooling.
### 1.2. Standard: Implement CI/CD Pipelines using GitHub Actions or Deno Deploy
**Do This:** Automate your build, test, and deployment processes using CI/CD platforms like GitHub Actions, Deno Deploy, or GitLab CI.
**Don't Do This:** Manually build and deploy your application.
**Why:** CI/CD ensures consistent builds, automated testing, and reliable deployments, reducing the risk of human error. Deno Deploy offers a streamlined experience directly integrated with the Deno runtime.
**Example (GitHub Actions):**
"""yaml
# .github/workflows/deno.yml
name: Deno CI/CD
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: '1.42.0'
- name: Run tests
run: deno task test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: '1.42.0'
- name: Build
run: deno task build
- name: Deploy to Deno Deploy
uses: denoland/deployctl@v1
with:
project: your-deno-deploy-project-name
entrypoint: dist/bundle.js
env:
DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }}
"""
**Explanation:** This workflow checks out the code, sets up Deno (version is VERY important for deterministic builds), runs tests, builds the application, and deploys it to Deno Deploy when changes are pushed to the "main" branch. Replace "your-deno-deploy-project-name" with your actual project name. The "DENO_DEPLOY_TOKEN" is stored as a secret in GitHub.
**Example (Deno Deploy):** Deno Deploy can automatically build and deploy directly from a GitHub repository. Configure the project within the Deno Deploy dashboard to specify the entry point and branch to monitor. No workflow file is needed in the repository itself in this case.
**Anti-Pattern:** Using different Deno versions in development, testing, and production environments. Always pin the Deno version to ensure consistency. The "denoland/setup-deno" action is crucial.
### 1.3. Standard: Implement Git Hooks for Pre-Commit Checks
**Do This:** Use "husky" or similar tools to implement Git hooks that automatically run linters, formatters, and tests before committing code.
**Don't Do This:** Rely on developers to manually run these checks.
**Why:** Git hooks prevent broken or unformatted code from being committed, improving code quality and reducing the load on CI systems.
**Example:**
1. **Install "husky":**
"""bash
deno add -p husky
"""
2. **Initialize Husky:**
"""bash
deno task husky install
"""
3. **Add a pre-commit hook:**
"""bash
deno task husky add .husky/pre-commit "deno task fmt && deno task lint && deno task test"
git add .husky/pre-commit
"""
**Explanation:** This setup ensures that "deno fmt", "deno lint", and "deno test" are executed before each commit. If any of these commands fail, the commit will be aborted. This is a *crucial* quality gate. The output of "deno add -p husky" must have "--allow-write", "--allow-read", and "--allow-env".
**Anti-Pattern:** Skipping pre-commit hooks or disabling them entirely.
## 2. Production Considerations
### 2.1. Standard: Bundle Your Application for Production
**Do This:** Use "deno bundle" to create a single JavaScript file for deployment.
**Don't Do This:** Deploy multiple source files or rely on dynamic imports in production.
**Why:** Bundling reduces the number of network requests, improves startup time, and simplifies deployment.
**Example:**
"""bash
deno task build
"""
(Assuming "deno.jsonc" contains the "build" task as defined in section 1.1)
**Explanation:** This command generates "dist/bundle.js", which contains all your application code and dependencies in a single file. The "--no-check" flag speeds up the bundling process by skipping type checking.
**Anti-Pattern:** Deploying unbundled code. This introduces unnecessary overhead and complexity, *especially* with remote imports.
### 2.2. Standard: Use Environment Variables for Configuration
**Do This:** Configure your application using environment variables.
**Don't Do This:** Hardcode sensitive information or configuration values in your source code.
**Why:** Environment variables allow you to configure your application without modifying the code, making it easier to deploy to different environments.
**Example:**
"""typescript
// main.ts
const port = parseInt(Deno.env.get("PORT") || "8000");
const dbUrl = Deno.env.get("DATABASE_URL") || "mongodb://localhost:27017";
console.log("Server listening on port ${port}");
// Example of using a default value if the variable is not set
const apiKey = Deno.env.get("API_KEY") ?? "default_api_key";
Deno.serve({ port }, (req) => {
return new Response("API Key: ${apiKey}");
});
"""
**Explanation:** This code retrieves the server port and database URL from environment variables. If the environment variables are not set, default values are used. The nullish coalescing operator "??" provides a concise way to provide default values.
**Anti-Pattern:** Committing ".env" files to your repository containing sensitive information.
### 2.3. Standard: Implement Logging and Monitoring
**Do This:** Use a logging library (e.g., "std/log") to log important events and errors. Integrate with a monitoring service (e.g., Prometheus, Grafana) to track application performance.
**Don't Do This:** Rely solely on "console.log" for production logging.
**Why:** Logging and monitoring provide insights into application behavior, enabling you to identify and resolve issues quickly.
**Example:**
"""typescript
// logger.ts
import * as log from "https://deno.land/std@0.217.0/log/mod.ts";
await log.setup({
handlers: {
console: new log.ConsoleHandler("INFO"),
file: new log.FileHandler("WARNING", {
filename: "./log.txt",
mode: "w",
formatter: "{datetime} {levelName} {msg}",
}),
},
loggers: {
default: {
level: "INFO",
handlers: ["console", "file"],
},
access: {
level: "DEBUG",
handlers: ["file"],
}
},
});
const logger = log.getLogger();
const accessLogger = log.getLogger("access");
export { logger, accessLogger };
// main.ts
import { logger, accessLogger } from "./logger.ts";
logger.info("Application started");
accessLogger.debug("Request received");
Deno.serve({ port: 8000 }, (req) => {
logger.info("Handling request");
accessLogger.info("${req.method} ${req.url}");
return new Response("Hello, Deno!");
});
"""
**Explanation:** This code configures a logger that writes to both the console and a file. It then uses the logger to log application events. Separate loggers can be customized for specific purposes.
**Anti-Pattern:** Logging excessive or irrelevant information, which can clutter logs and make it difficult to find important events. Also, failing to sanitize log data, which can lead to security vulnerabilities. Failing to rotate logs can cause disk space issues.
### 2.4. Standard: Implement Health Checks
**Do This:** Expose a health check endpoint (e.g., "/health") that returns the application's status.
**Don't Do This:** Assume the application is always running correctly.
**Why:** Health checks allow you to monitor the application's health and automatically restart it if necessary. This is crucial for container orchestration systems like Kubernetes.
**Example:**
"""typescript
// main.ts
Deno.serve({ port: 8000 }, (req) => {
if (req.url.endsWith("/health")) {
return new Response("OK", { status: 200 });
}
return new Response("Hello, Deno!");
});
"""
**Explanation:** This code exposes a "/health" endpoint that returns a 200 OK status when the application is running. More sophisticated health checks can check database connectivity, external API dependencies, and other critical services.
**Anti-Pattern:** Returning a 200 OK status even when the application is in a degraded state. The health check should accurately reflect the application's health.
### 2.5. Standard: Follow Security Best Practices
**Do This:** Adhere to security best practices, such as validating user input, escaping output, and using TLS for all network communication. Use tools like "deno lint" to find potential security vulnerabilities.
**Don't Do This:** Ignore security considerations or assume Deno is inherently secure.
**Why:** Security is paramount. Failing to address security vulnerabilities can lead to data breaches, service disruptions, and other serious consequences.
**Specific Deno Security Considerations:**
* **Permissions:** Deno's permission system provides a strong security model. Carefully grant only the necessary permissions to your application using flags like "--allow-net", "--allow-read", and "--allow-write". Avoid granting blanket permissions ("--allow-all").
* **Dependency Management:** Deno's explicit dependency management helps prevent supply chain attacks. Carefully review and audit your dependencies. Use integrity checks (e.g., "deno cache --lock=lock.json --lock-write") . Pin versions explicitly in "deno.jsonc".
* **Remote Imports:** Be cautious when using remote imports, as they can be vulnerable to tampering. Consider vendoring your dependencies (downloading them locally).
* **"Deno.unsafelyUnstable" APIs:** Deno exposes some features in the "Deno.unsafelyUnstable" namespace. These are generally to be avoided in production code, because they do not offer any backwards-compatibility guarantees.
**Example:** Using "--lock" files:
"""bash
deno cache --lock=lock.json --lock-write
"""
This command creates or updates the "lock.json" file, which contains the exact versions and integrity hashes of all your application's dependencies. This ensures that your application always uses the same dependencies, preventing unexpected behavior or security vulnerabilities. Commit this file to your repository. It provides a layer of assurance equivalent to "package-lock.json" in Node.js.
**Anti-Pattern:** Running your application with "--allow-all" in production.
## 3. Technology-Specific Details
### 3.1. Deno Deploy Specifics
* **Edge Functions:** Deno Deploy excels at deploying edge functions, which are small, serverless functions that run close to your users. Use edge functions for latency-sensitive operations.
* **Deploy Hooks:** Use deploy hooks to trigger actions after a successful deployment, such as invalidating CDNs or updating configuration files.
* **"deployctl":** Use the "deployctl" CLI tool for managing and deploying your Deno Deploy projects from the command line.
### 3.2. Containerization (Docker)
* **Multi-Stage Builds:** Use multi-stage Docker builds to create smaller and more secure images. Use a builder stage to compile your code and a runner stage to run your application. Use "scratch" or "alpine" as the base image for the runner stage.
* **Non-Root User:** Run your application as a non-root user within the container to reduce the risk of privilege escalation attacks.
* **Health Checks:** Integrate Docker health checks to automatically restart unhealthy containers.
**Example (Dockerfile):**
"""dockerfile
# Builder stage
FROM denoland/deno:1.42.0 AS builder
WORKDIR /app
COPY deno.json deno.json
COPY deps.ts deps.ts
RUN deno cache deps.ts # Cache dependencies
COPY . .
RUN deno task build
# Runner stage
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/dist/bundle.js .
EXPOSE 8000
CMD ["/usr/bin/deno", "run", "--allow-net", "--allow-read", "bundle.js"]
"""
**Explanation:** This Dockerfile uses a multi-stage build. The builder stage uses the "denoland/deno" image to build the application, and the runner stage uses "alpine" to create a minimal image. The "--allow-net" and "--allow-read" flags are explicitly specified in the "CMD" instruction, granting only the necessary permissions. The image built from this Dockerfile will only include the bundled JS, nothing else.
### 3.3. Database Migrations
* **Dedicated Migration Tool:** Use a dedicated migration tool to manage database schema changes. Consider libraries like "drizzle-orm" or similar tools with built-in migration support.
* **Idempotent Migrations:** Ensure that your migrations are idempotent, meaning they can be applied multiple times without causing errors.
* **Rollback Mechanism:** Implement a rollback mechanism to revert migrations in case of errors.
## 4. Common Anti-Patterns
* **Ignoring Errors:** Failing to handle errors properly can lead to unexpected behavior and crashes. Use "try...catch" blocks to handle errors gracefully.
* **Over-Engineering:** Avoid over-engineering your code or using unnecessary dependencies. Keep it simple and focus on solving the problem at hand. Remember Deno's philosophy emphasizes standard web APIs where possible.
* **Premature Optimization:** Don't optimize your code prematurely. Focus on writing clear, maintainable code first. Then, profile your application to identify performance bottlenecks and optimize those areas specifically.
* **Lack of Documentation:** Write clear and concise documentation for your code. Use JSDoc comments to document your API.
* **Deploying large, unoptimized images.** Make sure you're using smaller base images.
By adhering to these standards, Deno applications can be deployed reliably and maintained effectively, resulting in more robust and scalable systems.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Deno This document outlines the standards for creating reusable and maintainable components in Deno. It emphasizes best practices specific to Deno's unique features and promotes clean, modern coding patterns. ## 1. Principles of Component Design in Deno ### 1.1 Reusability Through Modularity * **Do This:** Design components with single, well-defined responsibilities. This promotes reusability across different parts of the application. * **Don't Do This:** Create monolithic components that perform multiple unrelated tasks. This hinders reusability and makes maintenance difficult. * **Why:** Modular components are easier to understand, test, and reuse in different contexts. """typescript // Good: Single responsibility - formatting dates export function formatDate(date: Date): string { return date.toLocaleDateString('en-US'); } // Bad: Multi-responsibility - formatting dates and logging export function formatDateAndLog(date: Date): string { const formattedDate = date.toLocaleDateString('en-US'); console.log("Date formatted: ${formattedDate}"); // Logging is not the responsibility of a date formatter return formattedDate; } """ ### 1.2 Abstraction and Encapsulation * **Do This:** Use interfaces and abstract classes to define contracts for components. Encapsulate internal implementation details to hide them from the outside world. * **Don't Do This:** Expose internal state or implementation details directly. This can lead to tight coupling and brittle code. * **Why:** Abstraction reduces complexity and allows you to change the implementation without affecting the clients. Encapsulation protects the component's integrity. """typescript // Good: Interface-based abstraction export interface Logger { log(message: string): void; } export class ConsoleLogger implements Logger { log(message: string): void { console.log(message); } } // Clients depend on the interface, not the concrete implementation export function useLogger(logger: Logger, message: string) { logger.log(message); } // Bad: Direct dependency on concrete class export class MyComponent { constructor(private logger: ConsoleLogger) {} // Tight coupling } """ ### 1.3 Separation of Concerns * **Do This:** Divide the application into distinct sections, each addressing a separate concern (e.g., data access, business logic, presentation). * **Don't Do This:** Mix concerns within a single component or module. * **Why:** Clear separation of concerns leads to better maintainability, testability, and scalability. """typescript // Good: Separated concerns // data_access.ts export async function fetchData(url: string) { const response = await fetch(url); return await response.json(); } // business_logic.ts export function processData(data: any) { // Perform calculations or transformations return data.map((item: any) => item.value * 2); } // presentation.ts import { fetchData } from './data_access.ts'; import { processData } from './business_logic.ts'; export async function displayData(url: string) { const data = await fetchData(url); const processedData = processData(data); console.log(processedData); } // Bad: Mixing concerns export async function displayAndProcessData(url: string) { const response = await fetch(url); const data = await response.json(); const processedData = data.map((item: any) => item.value * 2); console.log(processedData); } """ ### 1.4 Loose Coupling * **Do This:** Design components to minimize dependencies on each other. Use techniques like dependency injection and event-driven architectures. * **Don't Do This:** Create tightly coupled components that rely on specific implementations of other components. * **Why:** Loose coupling makes components more independent, easier to test in isolation, and less prone to cascading failures when one component changes. """typescript // Good: Loose coupling with dependency injection interface NotificationService { sendNotification(message: string): Promise<void>; } class EmailNotificationService implements NotificationService { async sendNotification(message: string): Promise<void> { console.log("Sending email: ${message}"); // Actual email sending logic would go here } } class SMSNotificationService implements NotificationService { async sendNotification(message: string): Promise<void> { console.log("Sending SMS: ${message}"); // Actual SMS sending logic would go here } } class UserRegistration { constructor(private notificationService: NotificationService) {} async registerUser(email: string): Promise<void> { // User registration logic await this.notificationService.sendNotification("Welcome, ${email}!"); } } // Using the components const emailService = new EmailNotificationService(); const registration = new UserRegistration(emailService); await registration.registerUser("test@example.com"); const smsService = new SMSNotificationService(); const registration2 = new UserRegistration(smsService); await registration2.registerUser("123-456-7890"); // Bad: Tight coupling (directly instantiating the EmailNotificationService inside UserRegistration) class UserRegistrationTight { private emailService : EmailNotificationService; constructor(){ this.emailService = new EmailNotificationService(); } async registerUser(email: string): Promise<void> { // User registration logic await this.emailService.sendNotification("Welcome, ${email}!"); } } """ ### 1.5 Single Source of Truth (SSOT) * **Do This:** Identify and define a single source of truth for each piece of information or configuration. * **Don't Do This:** Duplicate or hardcode the same information in multiple places. * **Why:** Ensures consistency and reduces the risk of conflicting information. Changes only need to be made in one place. """typescript // Good: Configuration from environment variables (single source of truth) const API_URL = Deno.env.get("API_URL") || "http://localhost:8000"; //default if not available export async function fetchData(endpoint: string) { const response = await fetch("${API_URL}/${endpoint}"); return await response.json(); } // Bad: Hardcoding the API URL in multiple places export async function fetchDataBad(endpoint: string) { const response = await fetch("http://localhost:8000/${endpoint}"); //URL hardcoded here return await response.json(); } export async function anotherFunctionBad(endpoint: string) { const response = await fetch("http://localhost:8000/${endpoint}"); //AND here; not DRY return await response.json(); } """ ## 2. Deno-Specific Component Design Considerations ### 2.1 Leveraging the Standard Library * **Do This:** Utilize the Deno standard library whenever possible for common tasks (e.g., file system operations, HTTP handling). * **Don't Do This:** Reimplement functionality that is already available in the standard library. * **Why:** The standard library is well-maintained, tested, and optimized for Deno. It’s a single source of truth for common functionalities. """typescript // Good: Using the standard library for file system operations import { readTextFile } from "https://deno.land/std@0.212.0/fs/mod.ts"; async function loadConfig(filePath: string): Promise<string> { return await readTextFile(filePath); // Using standard library } // Bad: Implementing file reading logic manually async function loadConfigManual(filePath: string): Promise<string> { // Replicating readTextFile's logic const file = await Deno.open(filePath); const decoder = new TextDecoder("utf-8"); const contents = decoder.decode(await Deno.readAll(file)); Deno.close(file.rid); return contents; } """ ### 2.2 Dependency Management * **Do This:** Use explicit import maps to manage dependencies and ensure that your code uses consistent versions of libraries. * **Don't Do This:** Rely on implicit resolution or hardcoded URLs for dependencies. This can lead to dependency conflicts and unpredictable behavior. * **Why:** Import maps improve reproducibility and help prevent dependency-related issues. Create an import map ("import_map.json"): """json { "imports": { "fmt/": "https://deno.land/std@0.212.0/fmt/", "lodash": "https://cdn.skypack.dev/lodash" } } """ Use the import map: """typescript // Good: Using import maps import { format } from "fmt/datetime.ts"; // Resolves to std@0.212.0 correctly import * as _ from "lodash"; console.log(format(new Date(), "yyyy-MM-dd")); console.log(_.chunk([1,2,3,4,5], 2)) // Bad: Hardcoded URLs import { format as formatBad } from "https://deno.land/std@0.211.0/fmt/datetime.ts"; // Version is hardcoded, creating potential issues """ Run the code with the import map: """bash deno run --import-map import_map.json your_file.ts """ ### 2.3 Permissions Model * **Do This:** Request only the minimum necessary permissions for each component. Follow the principle of least privilege. * **Don't Do This:** Request blanket permissions (e.g., "--allow-all") unless absolutely necessary. * **Why:** Limiting permissions enhances the security of your application. """typescript // Good: Requesting specific permissions // deno run --allow-read=./data.txt --allow-net=api.example.com your_script.ts async function readFile(filePath: string): Promise<string> { try { return await Deno.readTextFile(filePath); } catch (error) { console.error("Error reading file: ${error}"); return ""; } } async function fetchFromAPI(url: string): Promise<any> { try { const response = await fetch(url); return await response.json(); } catch (error) { console.error("Error fetching data: ${error}"); return null; } } // Bad: Using --allow-all // deno run --allow-all your_script.ts // Avoid unless absolutely required """ ### 2.4 Asynchronous Operations and "async"/"await" * **Do This:** Embrace asynchronous operations and "async"/"await" syntax for non-blocking I/O and other long-running tasks. * **Don't Do This:** Use synchronous operations that can block the event loop. * **Why:** Asynchronous operations improve the responsiveness and scalability of your application. """typescript // Good: Asynchronous file reading async function processFile(filePath: string): Promise<void> { try { const contents = await Deno.readTextFile(filePath); console.log("File contents: ${contents.substring(0, 100)}"); // Show first 100 chars } catch (error) { console.error("Error processing file: ${error}"); } } // Bad: Synchronous file reading (blocking) Avoid! function processFileSync(filePath: string): void { try { const contents = Deno.readTextFileSync(filePath); //This could block the event loop console.log("File contents: ${contents.substring(0, 100)}"); // Show first 100 chars } catch (error) { console.error("Error processing file: ${error}"); } } """ ### 2.5 Error Handling * **Do This:** Implement robust error handling using "try...catch" blocks, and consider custom error types for better context. * **Don't Do This:** Ignore errors or let exceptions propagate unhandled. * **Why:** Proper error handling prevents application crashes and provides useful debugging information. """typescript // Good: Custom error handling class APIError extends Error { constructor(message: string, public statusCode: number) { super(message); this.name = "APIError"; } } async function fetchDataWithErrorHandling(url: string): Promise<any> { try { const response = await fetch(url); if (!response.ok) { throw new APIError("HTTP error! status: ${response.status}", response.status); } return await response.json(); } catch (error) { if (error instanceof APIError) { console.error("API Error: ${error.message}, Status Code: ${error.statusCode}"); // Log the error appropriately. Don't just print to console in production. } else { console.error("Unexpected error: ${error}"); } return null; // Or re-throw, depending on your needs. } } // Bad: Ignoring errors (dangerous!) async function fetchDataIgnoringErrors(url: string): Promise<any> { const response = await fetch(url); //No error handling return await response.json(); //No error handling } """ ## 3. Design Patterns for Deno Components ### 3.1 Factory Pattern * **Do This:** Use the factory pattern to create objects of different types based on runtime conditions. * **Why:** Decouples object creation from the client code and allows for easy extensibility. """typescript interface PaymentGateway { processPayment(amount: number): Promise<boolean>; } class StripePaymentGateway implements PaymentGateway { async processPayment(amount: number): Promise<boolean> { console.log("Processing $${amount} via Stripe"); return true; // Simulate success } } class PayPalPaymentGateway implements PaymentGateway { async processPayment(amount: number): Promise<boolean> { console.log("Processing $${amount} via PayPal"); return true; // Simulate success } } class PaymentGatewayFactory { static createPaymentGateway(type: "stripe" | "paypal"): PaymentGateway { switch (type) { case "stripe": return new StripePaymentGateway(); case "paypal": return new PayPalPaymentGateway(); default: throw new Error("Unsupported payment gateway type: ${type}"); } } } // Usage const stripeGateway = PaymentGatewayFactory.createPaymentGateway("stripe"); await stripeGateway.processPayment(100); const paypalGateway = PaymentGatewayFactory.createPaymentGateway("paypal"); await paypalGateway.processPayment(50); """ ### 3.2 Observer Pattern * **Do This:** Employ the observer pattern to create loosely coupled components that can react to events. * **Why:** Allows for a publish-subscribe mechanism where components can subscribe to events and be notified when they occur. """typescript interface Observer { update(message: string): void; } class Subject { private observers: Observer[] = []; attach(observer: Observer): void { this.observers.push(observer); } detach(observer: Observer): void { this.observers = this.observers.filter((obs) => obs !== observer); } notify(message: string): void { for (const observer of this.observers) { observer.update(message); } } } class ConcreteObserver implements Observer { constructor(private name: string) {} update(message: string): void { console.log("${this.name} received: ${message}"); } } // Usage const subject = new Subject(); const observer1 = new ConcreteObserver("Observer 1"); const observer2 = new ConcreteObserver("Observer 2"); subject.attach(observer1); subject.attach(observer2); subject.notify("Hello, observers!"); subject.detach(observer2); subject.notify("Another message!"); """ ### 3.3 Strategy Pattern * **Do This:** Use the strategy pattern to define a family of algorithms, encapsulate each one, and make them interchangeable. * **Why:** Enables you to select an algorithm at runtime and easily add new algorithms without modifying the client code. """typescript interface SortStrategy { sort(data: number[]): number[]; } class BubbleSortStrategy implements SortStrategy { sort(data: number[]): number[] { console.log("Using Bubble Sort"); // Bubble sort implementation (simplified) return [...data].sort((a, b) => a - b); } } class QuickSortStrategy implements SortStrategy { sort(data: number[]): number[] { console.log("Using Quick Sort"); // Quick sort implementation (simplified) - uses standard sort for brevity return [...data].sort((a, b) => a - b); } } class Sorter { constructor(private strategy: SortStrategy) {} setStrategy(strategy: SortStrategy): void { this.strategy = strategy; } sort(data: number[]): number[] { return this.strategy.sort(data); } } // Usage const data = [5, 2, 8, 1, 9, 4]; const bubbleSorter = new Sorter(new BubbleSortStrategy()); const sortedData1 = bubbleSorter.sort(data); console.log(sortedData1); const quickSorter = new Sorter(new QuickSortStrategy()); const sortedData2 = quickSorter.sort(data); console.log(sortedData2); quickSorter.setStrategy(new BubbleSortStrategy()) const sortedData3 = quickSorter.sort(data); // Now uses bubble sort console.log(sortedData3); """ ## 4. Testing Deno Components ### 4.1 Unit Testing * **Do This:** Write unit tests for each component to verify its functionality in isolation. Use Deno's built-in testing framework ("Deno.test"). * **Don't Do This:** Skip unit tests or write tests that are too broad in scope. * **Why:** Unit tests help you catch bugs early and ensure that your components behave as expected. """typescript // Example: Unit test for the formatDate function import { formatDate } from "./date_utils.ts"; import { assertEquals } from "https://deno.land/std@0.212.0/assert/mod.ts"; Deno.test("formatDate should return a formatted date string", () => { const date = new Date(2024, 0, 20); // January 20, 2024 const formattedDate = formatDate(date); assertEquals(formattedDate, "1/20/2024"); }); """ ### 4.2 Integration Testing * **Do This:** Write integration tests to verify that different components work together correctly. Simulate real-world scenarios. * **Don't Do This:** Assume that components will work together correctly without testing their interactions. * **Why:** Integration tests help you detect issues that may arise when different parts of your application interact. """typescript //Example (basic): Check that an http endpoint returns expected json import { assertEquals } from "https://deno.land/std@0.212.0/assert/mod.ts"; Deno.test("Integration: Fetch data and verify structure", async () => { const response = await fetch("https://rickandmortyapi.com/api/character/1"); //Real endpoint, but could be "localhost:8000" for a local Deno server assertEquals(response.status, 200); const data = await response.json(); assertEquals(data.name, "Rick Sanchez"); assertEquals(data.status, "Alive"); }); """ ### 4.3 Mocking * **Do This:** Use mocking libraries (e.g., "std/testing/mock") to isolate components during testing. * **Don't Do This:** Rely on real dependencies in unit tests, as this can make tests slow and unreliable. * **Why:** Mocking allows you to control the behavior of dependencies and focus on testing the logic of a single component. """typescript // Example: Mocking the fetch API import { stub } from "https://deno.land/std@0.212.0/testing/mock.ts"; import { assertEquals } from "https://deno.land/std@0.212.0/assert/mod.ts"; async function fetchData(url: string): Promise<any> { const response = await fetch(url); return await response.json(); } Deno.test("fetchData should return mocked data", async () => { const mockData = { message: "Mocked data" }; const fetchStub = stub( globalThis, "fetch", () => Promise.resolve({ json: () => Promise.resolve(mockData) } as Response), ); const data = await fetchData("https://example.com/api"); assertEquals(data, mockData); fetchStub.restore(); // Restore the original fetch function }); """ ## 5. Documentation and Style ### 5.1 JSDoc Comments * **Do This:** Use JSDoc comments to document all exported functions, classes, and interfaces. * **Don't Do This:** Omit documentation or write vague and unhelpful comments. * **Why:** JSDoc comments make your code easier to understand and use. They can also be used to generate API documentation. """typescript /** * Formats a date object into a string. * * @param {Date} date The date to format. * @returns {string} The formatted date string (e.g., "YYYY-MM-DD"). */ export function formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return "${year}-${month}-${day}"; } """ ### 5.2 Code Formatting * **Do This:** Use a code formatter (e.g., "deno fmt") to ensure consistent code style. * **Don't Do This:** Rely on manual formatting, which can lead to inconsistencies. * **Why:** Consistent code style improves readability and reduces cognitive load. Run "deno fmt" to automatically format your code. ### 5.3 Code Reviews * **Do This:** Conduct code reviews to ensure that code meets the established standards and best practices. * **Don't Do This:** Merge code without review or ignore feedback from reviewers. * **Why:** Code reviews help catch errors, improve code quality, and promote knowledge sharing.
# Core Architecture Standards for Deno This document outlines the core architectural standards and best practices for Deno projects. These standards are designed to promote maintainability, scalability, performance, and security. They focus on fundamental architectural patterns, project structure, and organizational principles within the Deno runtime environment. ## 1. Architectural Patterns Choosing the right architectural pattern is crucial for building robust and scalable Deno applications. Here are some recommended patterns: ### 1.1 Microservices Microservices offer independent deployability, scalability, and technology diversity. **Do This:** * Design services around specific business capabilities. * Use lightweight communication protocols such as HTTP/REST or gRPC. * Implement proper service discovery and registration mechanisms. * Ensure each service has its own database and data model. * Utilize Deno's built-in testing framework for thorough unit and integration testing of each service. **Don't Do This:** * Create a monolithic application disguised as microservices. * Share databases between services. * Introduce tight coupling between services. * Neglect proper monitoring and logging across all services. **Why This Matters:** Microservices enhance agility and allow for independent scaling and deployment. Proper implementation prevents cascading failures and ensures resilience. **Example:** """typescript // Service A: User service import { serve } from "https://deno.land/std@0.212.0/http/server.ts"; async function handler(request: Request): Promise<Response> { const url = new URL(request.url); if (url.pathname === "/users") { return new Response(JSON.stringify([{ id: 1, name: "Alice" }, { id: 2, name: "Bob"}]), { headers: { "content-type": "application/json" }, }); } return new Response("Not Found", { status: 404 }); } serve(handler, { port: 3000 }); console.log("User service listening on port 3000"); """ """typescript // Service B: Order service import { serve } from "https://deno.land/std@0.212.0/http/server.ts"; async function handler(request: Request): Promise<Response> { const url = new URL(request.url); if (url.pathname === "/orders") { return new Response(JSON.stringify([{ userId: 1, orderId: "A123" }, { userId: 2, orderId: "B456"}]), { headers: { "content-type": "application/json" }, }); } return new Response("Not Found", { status: 404 }); } serve(handler, { port: 3001 }); console.log("Order service listening on port 3001"); """ ### 1.2 Layered Architecture A layered architecture organizes code into distinct layers with specific responsibilities (e.g., presentation, business logic, data access). **Do This:** * Define clear boundaries between layers. * Enforce the dependency rule: layers can only depend on layers directly below them. * Use dependency injection to manage dependencies between layers. * Employ interfaces to decouple layers, promoting testability and flexibility. **Don't Do This:** * Create tight coupling between layers. * Allow layers to directly access data or functionality in other layers without going through a defined interface. * Implement circular dependencies between layers. **Why This Matters:** Layered architecture improves code organization, maintainability, and testability by separating concerns. **Example:** """typescript // data_access.ts export interface UserRepository { getUser(id: number): Promise<{ id: number; name: string } | undefined>; } export class InMemoryUserRepository implements UserRepository { private users: { id: number; name: string }[] = [{ id: 1, name: "John" }, { id: 2, name: "Jane" }]; async getUser(id: number): Promise<{ id: number; name: string } | undefined> { return this.users.find(user => user.id === id); } } """ """typescript // business_logic.ts import { UserRepository } from "./data_access.ts"; export class UserService { private userRepository: UserRepository; constructor(userRepository: UserRepository) { this.userRepository = userRepository; } async getUserName(id: number): Promise<string | undefined> { const user = await this.userRepository.getUser(id); return user?.name; } } """ """typescript // presentation.ts import { UserService, } from "./business_logic.ts"; import { InMemoryUserRepository } from "./data_access.ts"; const userRepository = new InMemoryUserRepository(); const userService = new UserService(userRepository); async function main() { const userName = await userService.getUserName(1); console.log(userName); // Output: John } main(); """ ### 1.3 Event-Driven Architecture Event-Driven Architecture (EDA) facilitates asynchronous communication between services, enabling decoupled and scalable systems. **Do This:** * Use message queues (e.g., RabbitMQ, Apache Kafka) for reliable message delivery. * Define clear event schemas (e.g., using JSON Schema). * Implement idempotent consumers to handle duplicate messages. * Monitor event flows and handle potential errors gracefully. **Don't Do This:** * Create tight coupling between event producers and consumers. * Rely on synchronous communication for critical operations. * Ignore event sequencing and ordering requirements. **Why This Matters:** EDA promotes scalability, resilience, and responsiveness by decoupling services and enabling asynchronous communication. **Example (using an in-memory event bus for simplicity – not production-ready):** """typescript // event_bus.ts type EventHandler<T> = (event: T) => void; class EventBus { private handlers: { [key: string]: EventHandler<any>[] } = {}; subscribe<T>(event: string, handler: EventHandler<T>) { if (!this.handlers[event]) { this.handlers[event] = []; } this.handlers[event].push(handler); } publish<T>(event: string, data: T) { const handlers = this.handlers[event]; if (handlers) { handlers.forEach(handler => handler(data)); } } } export const eventBus = new EventBus(); """ """typescript // user_service.ts import { eventBus } from "./event_bus.ts"; interface UserCreatedEvent { userId: number; userName: string; } class UserService { createUser(userId: number, userName: string) { // ... create user logic ... const event: UserCreatedEvent = { userId, userName }; eventBus.publish<UserCreatedEvent>("user.created", event); console.log("User created event published for user ${userId}"); } } export const userService = new UserService(); """ """typescript // email_service.ts import { eventBus } from "./event_bus.ts"; interface UserCreatedEvent { userId: number; userName: string; } class EmailService { constructor() { eventBus.subscribe<UserCreatedEvent>("user.created", (event) => { console.log("Sending welcome email to ${event.userName} (user ID: ${event.userId})"); // ... email sending logic ... }); } } export const emailService = new EmailService(); """ """typescript // main.ts import { userService } from "./user_service.ts"; import { emailService } from "./email_service.ts"; // Ensure email service subscribes userService.createUser(123, "Alice"); // This will trigger the email service. """ ## 2. Project Structure A well-defined project structure is essential for code discoverability and maintainability. ### 2.1 Recommended Structure """ my-deno-project/ ├── src/ # Source code │ ├── controllers/ # Handles HTTP requests and responses │ ├── services/ # Business logic │ ├── repositories/ # Data access layer │ ├── models/ # Data models and interfaces │ ├── utils/ # Reusable utility functions │ ├── middleware/ # Middleware functions │ ├── config/ # Configuration files │ ├── app.ts # Main application entry point │ └── routes.ts # HTTP route definitions ├── tests/ # Unit and integration tests │ ├── controllers/ │ ├── services/ │ └── ... ├── .env # Environment variables ├── deno.jsonc # Deno configuration file ├── README.md # Project documentation └── .gitignore # Ignored files """ **Do This:** * Organize code into logical directories based on functionality. * Use clear and descriptive names for files and directories. * Keep related files together. * Use "src" directory for the application source code. * Include a "tests" directory alongside the "src" directory. **Don't Do This:** * Place all files in a single directory. * Use cryptic or ambiguous names. * Mix different types of files in the same directory. * Commit sensitive information (like API keys) to the code repository. Use ".env" and load environment variables. **Why This Matters:** A consistent and well-structured project makes it easier for developers to navigate, understand, and maintain the codebase. **Example:** "deno.jsonc": """jsonc { "fmt": { "files": [ "src/", "tests/" ], "options": { "useTabs": false, "lineWidth": 120, "indentWidth": 2, "singleQuote": true, "proseWrap": "always" } }, "lint": { "files": [ "src/", "tests/" ], "rules": { "tags": [ "recommended" ] } }, "tasks": { "start": "deno run --allow-net --allow-read src/app.ts", "test": "deno test --allow-read --allow-net tests/" }, "importMap": "import_map.json" } """ "import_map.json": """json { "imports": { "oak/": "https://deno.land/x/oak@v12.6.1/", "std/": "https://deno.land/std@0.212.0/" } } """ ### 2.2 Modularization Splitting code into modules improves reusability and maintainability. **Do This:** * Break down complex functionalities into smaller, self-contained modules. * Export only the necessary symbols from each module. * Use descriptive names for modules. * Employ Deno's module import syntax (e.g., "import { ... } from "./module.ts";"). * Favor explicit imports over wildcard imports ("import * as ..."). **Don't Do This:** * Create large, monolithic modules. * Export unnecessary symbols. * Create circular dependencies between modules. **Why This Matters:** Modularization promotes code reusability, testability, and maintainability by isolating functionality and reducing dependencies. **Example:** """typescript // src/utils/string_utils.ts export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } export function reverse(str: string): string { return str.split("").reverse().join(""); } """ """typescript // src/services/greeting_service.ts import { capitalize } from "../utils/string_utils.ts"; export function greet(name: string): string { return "Hello, ${ capitalize(name) }!"; } """ """typescript // src/controllers/greeting_controller.ts import { greet } from "../services/greeting_service.ts"; import { Context } from "oak/mod.ts"; export const greetingController = async (ctx: Context) => { const name = ctx.params.name || "World"; ctx.response.body = greet(name); }; """ ## 3. Dependency Management Managing dependencies effectively is crucial for project stability and security. ### 3.1 Import Maps Use import maps to manage external dependencies and improve code readability. Deno promotes the use of explicit versions for dependencies. **Do This:** * Define dependencies in an "import_map.json" file. * Use logical names for dependencies (e.g., "oak/" instead of the full URL). * Update dependencies regularly to address security vulnerabilities and bug fixes. * Pin dependency versions for reproducible builds. **Don't Do This:** * Use bare import specifiers. * Mix relative and absolute imports. * Ignore security updates. Don't use wildcard versions (e.g. "*", "latest"). **Why This Matters:** Import maps enhance code readability, simplify dependency management, and improve security by controlling the versions of imported modules. **Example:** See the "deno.jsonc" example above for how "import_map.json" is referenced. See the "import_map.json" example above for its basic structure. ### 3.2 Dependency Injection Use dependency injection to decouple components and improve testability. **Do This:** * Use constructor injection or setter injection. * Define interfaces for dependencies. * Use a dependency injection framework (e.g., "dIContainer") for complex applications. **Don't Do This:** * Create tight coupling between components. * Use global state or singletons excessively. * Instantiate dependencies directly within components. **Why This Matters:** Dependency injection promotes loose coupling, improves testability, and allows for easier component substitution. **Example:** """typescript // logger.ts export interface Logger { log(message: string): void; } export class ConsoleLogger implements Logger { log(message: string): void { console.log(message); } } """ """typescript // app.ts import { Logger, ConsoleLogger } from "./logger.ts"; class MyApp { private logger: Logger; constructor(logger: Logger) { this.logger = logger; } run() { this.logger.log("Application started"); } } const logger = new ConsoleLogger(); const app = new MyApp(logger); app.run(); """ ## 4. Error Handling Robust error handling is critical for application stability. ### 4.1 Explicit Error Handling Handle errors explicitly using "try...catch" blocks and "Result" types. **Do This:** * Use "try...catch" blocks to handle potential errors. * Wrap asynchronous operations in "try...catch" blocks. * Consider using a "Result" type (either a custom implementation or a library like "neverthrow") to represent success or failure. * Log errors with sufficient context (e.g., timestamp, error message, stack trace). * Implement graceful error handling at the application level (e.g., display a user-friendly error message). **Don't Do This:** * Ignore errors or swallow exceptions. * Rely solely on unhandled promise rejections. * Expose sensitive error information to the client. **Why This Matters:** Explicit error handling prevents application crashes, provides valuable debugging information, and ensures a better user experience. **Example:** """typescript async function fetchData(url: string): Promise<string | Error> { try { const response = await fetch(url); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.text(); return data; } catch (error) { console.error("Error fetching data:", error); return error instanceof Error ? error : new Error(String(error)); } } async function main() { const result = await fetchData("https://example.com"); if (result instanceof Error) { console.error("Failed to fetch data:", result.message); } else { console.log("Data:", result); } } main(); """ ### 4.2 Custom Error Types Define custom error types to provide more specific error information. **Do This:** * Create custom error classes that extend the built-in "Error" class. * Include additional properties in custom error types to provide more context (e.g., error code, resource ID). * Use discriminated unions for error types to simplify error handling. **Don't Do This:** * Rely solely on generic "Error" objects. * Throw primitive values as errors. **Why This Matters:** Custom error types improve code readability, simplify error handling, and provide more context for debugging. **Example:** """typescript class UserNotFoundError extends Error { constructor(public userId: number, message: string = "User with ID ${userId} not found") { super(message); this.name = "UserNotFoundError"; } } async function getUser(id: number): Promise<{ id: number; name: string } | UserNotFoundError> { // Simulate user lookup if (id !== 1) { return new UserNotFoundError(id); } return { id: 1, name: "John" }; } async function main() { const user = await getUser(2); if (user instanceof UserNotFoundError) { console.error("Error: ${user.message}"); } else { console.log("User:", user); } } main(); """ ## 5. Security Security is paramount in any application. ### 5.1 Secure Coding Practices Follow secure coding practices to prevent common vulnerabilities. **Do This:** * Sanitize user inputs to prevent injection attacks (e.g., SQL injection, XSS). * Use prepared statements or parameterized queries for database interactions. * Implement proper authentication and authorization mechanisms. * Enforce the principle of least privilege. * Use HTTPS for all communication to protect data in transit. * Keep dependencies up to date to address security vulnerabilities. * Use a linter with security rules enabled. **Don't Do This:** * Trust user inputs without validation. * Store sensitive information in plain text. * Expose sensitive information in error messages. * Disable security features or ignore security warnings. **Why This Matters:** Secure coding practices protect applications from common vulnerabilities and prevent data breaches. **Example (input sanitization using a library):** """typescript import { escape } from "https://deno.land/x/escape@v2.0.0/mod.ts"; function sanitizeInput(input: string): string { return escape(input); } const userInput = "<script>alert('XSS');</script>"; const sanitizedInput = sanitizeInput(userInput); console.log("Sanitized input:", sanitizedInput); // Output: <script>alert('XSS');</script> """ ### 5.2 Deno Permissions Leverage Deno's permission system to restrict access to system resources. **Do This:** * Grant only the necessary permissions to each application. * Use specific permissions instead of broad permissions (e.g., "--allow-read=/path/to/file" instead of "--allow-read"). * Review permissions regularly. * Use "--deny" flags during development to identify missing permissions. **Don't Do This:** * Grant all permissions ("--allow-all"). * Ignore permission warnings. * Run untrusted code with elevated permissions. **Why This Matters:** Deno's permission system limits the impact of security vulnerabilities by restricting access to sensitive system resources. **Example:** Running a Deno script with the "--allow-net" flag: """bash deno run --allow-net=example.com src/app.ts """ This allows the script to make network requests only to "example.com". Any attempt to make requests to other domains will be blocked. ## 6. Testing Comprehensive testing is essential for ensuring code quality. ### 6.1 Unit Tests Write unit tests to verify the functionality of individual components. **Do This:** * Use Deno's built-in testing framework ("deno test"). * Write tests for all critical code paths. * Use mocking and stubbing to isolate components during testing. * Follow the Arrange-Act-Assert pattern. * Aim for high test coverage. **Don't Do This:** * Write tests that are too complex or tightly coupled to the implementation. * Skip testing error handling logic. * Ignore failing tests. **Why This Matters:** Unit tests provide early feedback on code quality, prevent regressions, and improve code maintainability. **Example:** """typescript // src/utils/math.ts export function add(a: number, b: number): number { return a + b; } """ """typescript // tests/utils/math_test.ts import { add } from "../../src/utils/math.ts"; import { assertEquals } from "https://deno.land/std@0.212.0/assert/mod.ts"; Deno.test("add() should return the sum of two numbers", () => { assertEquals(add(2, 3), 5); assertEquals(add(-1, 1), 0); assertEquals(add(0, 0), 0); }); """ ### 6.2 Integration Tests Write integration tests to verify the interaction between multiple components. **Do This:** * Test the integration between different modules, services, or systems. * Use real or mock dependencies for integration testing. * Verify data flow and communication between components. * Test error handling and edge cases. **Don't Do This:** * Skip integration tests. * Rely solely on unit tests. * Make integration tests too brittle or dependent on external factors. **Why This Matters:** Integration tests verify that components work together correctly, ensuring that the system functions as a whole. ## 7. Documentation Comprehensive documentation is crucial for code maintainability and onboarding new developers. ### 7.1 Code Comments Write clear and concise code comments to explain complex logic and implementation details. **Do This:** * Use JSDoc-style comments for documenting functions, classes, and interfaces. * Explain the purpose, parameters, and return values of functions. * Document complex algorithms and data structures. * Update comments when the code changes. **Don't Do This:** * Write redundant or obvious comments. * Use comments to explain poorly written code. Refactor instead. * Let comments become outdated. **Why This Matters:** Code comments improve code readability and make it easier for developers to understand and maintain the codebase. As the linked Deno Style Guide states: "We strive for complete documentation. Every exported symbol ideally should have a JSDoc comment." **Example:** """typescript /** * Adds two numbers together. * * @param a - The first number. * @param b - The second number. * @returns The sum of the two numbers. */ export function add(a: number, b: number): number { return a + b; } """ ### 7.2 Project Documentation Create project-level documentation to provide an overview of the application architecture, design, and usage. **Do This:** * Include a README file with a description of the project, setup instructions, and usage examples. * Use a documentation generator (e.g., Deno doc) to generate API documentation from JSDoc comments. * Document the application architecture, design patterns, and key decisions. * Provide examples of how to use the application or library. **Don't Do This:** * Skip project-level documentation. * Let documentation become outdated. * Assume that developers can understand the application without documentation. **Why This Matters:** Project documentation provides a comprehensive overview of the application, making it easier for developers to understand, use, and maintain the codebase.
# State Management Standards for Deno This document outlines the coding standards for state management in Deno applications. It aims to provide a clear and consistent approach to managing application state, data flow, and reactivity, ensuring maintainability, performance, and security. ## 1. Introduction to State Management in Deno State management is a crucial aspect of any application, especially as complexity grows. In Deno, choosing the right strategy and tools can significantly impact performance, scalability, and developer experience. Unlike the browser environment, Deno gives developers more control over the runtime and access to lower-level APIs. Because of this, the state management solutions often need to be re-evaluated. This document covers several acceptable approaches, ranging from simple component-level state to more comprehensive solutions for large applications. We advocate for clarity, predictability, and maintainability in managing state. ## 2. Core Principles * **Single Source of Truth:** Each piece of state should have a single, authoritative source. This avoids inconsistencies and makes debugging easier. * **Immutability:** Treat state as immutable whenever possible. Modifying state by creating new objects, rather than mutating existing ones, simplifies reasoning about state changes and enables efficient change detection. * **Explicit Data Flow:** Make data dependencies and state changes explicit and predictable. Avoid implicit or hidden state mutations. * **Separation of Concerns:** Keep state management logic separate from UI rendering and business logic to improve maintainability and testability. * **Minimization:** Only store the necessary state. Avoid storing derived or computed values directly, instead calculating them on demand. ## 3. Approaches to State Management ### 3.1. Simple Component-Level State For smaller components or applications, simple state management techniques can suffice. This involves using variables within a component or module to hold the state. **Do This:** * Use "let" or "const" to declare state variables within a component or module. * Employ simple event handlers or functions to update the state directly. * Consider using "useState" if integrating with a frontend framework like React (via Fresh). * For simple data dependencies, consider using computed properties if your framework allows (e.g., Fresh). **Don't Do This:** * Avoid creating global variables to store application state directly. This makes the state difficult to track and manage. * Don't mutate state directly without triggering updates in your UI framework (if applicable). Direct mutation can lead to stale data and unexpected behavior. * Avoid deeply nested or complex state objects if simpler alternatives exist. **Example (Vanilla Deno - No Framework Usage):** """typescript // counter.ts let count = 0; export function increment() { count++; console.log("Count:", count); // Notify any UI components that are tracking this state (if applicable) } export function getCount() { return count; } // main.ts import { increment, getCount } from "./counter.ts"; increment(); increment(); console.log("Current count:", getCount()); """ **Why:** Using local variables encapsulates the state within a module, preventing unintended modifications from other parts of the application. Functions like "increment" provide a controlled way to update the state, offering an opportunity to trigger UI updates or other side effects. **Common Mistakes:** * Forgetting to export state update functions. * Modifying state outside its defining module. ### 3.2. Using Signals (Preact Signals, Effector, etc.) Signals offer a reactive approach to state management where changes to a signal automatically trigger updates in components or other reactive expressions. They are efficient and granular, only updating when the signal's value changes. **Do This:** * Choose a signals library suitable for your needs (Preact Signals, Effector, etc.). Preact Signals is well-suited for fine-grained component updates within a Preact/Fresh application. * Create signals to hold your state values. * Use computed signals to derive values from other signals. * Update signals using the ".value" property. * Utilize effects to perform side effects in response to signal changes. **Don't Do This:** * Overuse signals. Not every variable needs to be a signal. Focus on data that requires reactive updates. * Mutate signal values directly without using the ".value" property. * Create circular dependencies between computed signals. **Example (Preact Signals with Deno Fresh):** """tsx // app.tsx (Deno Fresh component) import { signal, computed } from "@preact/signals"; import { useState } from "preact/hooks"; const count = signal(0); const doubledCount = computed(() => count.value * 2); export default function Counter() { const [localState, setLocalState] = useState(0); const increment = () => { count.value++; }; const incrementLocal = () => { setLocalState(localState + 1); } return ( <div> <p>Count: {count.value}</p> <p>Doubled Count: {doubledCount.value}</p> <button onClick={increment}>Increment Count</button> <p>Local State: {localState}</p> <button onClick={incrementLocal}>Increment Local State</button> </div> ); } """ **Why:** Signals provide a more efficient way to update components than traditional virtual DOM diffing. Only components that depend on a changed signal will re-render. Computed signals automatically update when their dependencies change, ensuring values are always consistent. **Common Mistakes:** * Forgetting to include "@preact/signals" in your Fresh project. "deno add @preact/signals" solves this. * Trying to directly modify signal values (e.g. "count = 5;" instead of "count.value = 5;"). * Over-relying on signals for values that don't genuinely need to be reactive, which can lead to unnecessary performance overhead. ### 3.3. Context API (React-like Approach) The Context API allows you to share state between components without explicitly passing props through every level of the component tree. This is useful for themes, user authentication, or other global state. **Do This:** * Create a context using "React.createContext()" (if using React/Preact through Fresh/Islands architecture) * Create a provider component using "context.Provider" to wrap the part of the application that needs access to the context. * Access the context value using "React.useContext(context)". (if using React/Preact through Fresh/Islands architecture) **Don't Do This:** * Overuse the Context API. It's best suited for data that is used by many components, not for isolated component state. * Mutate the context value directly within components. Use a separate state management solution (e.g., "useState", signals, or a more robust library) to manage the context value. **Example (Deno Fresh Context API - note the requirement for React/Preact dependencies):** """tsx // context.ts import React, { createContext, useState, useContext } from "preact/compat"; interface AuthContextType { user: string | null; login: (user: string) => void; logout: () => void; } const AuthContext = createContext<AuthContextType>({ user: null, login: () => {}, logout: () => {}, }); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<string | null>(null); const login = (user: string) => { setUser(user); }; const logout = () => { setUser(null); }; const value: AuthContextType = { user, login, logout, }; return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> ); } export function useAuth() { return useContext(AuthContext); } // _layout.tsx (wrapping the app) /** @jsx h */ import { h } from 'preact'; import { AuthProvider } from "./context.ts"; export default function Layout({ children }: { children: React.ReactNode }) { return ( <AuthProvider> {children} </AuthProvider> ); } // profile.tsx (using the context) /** @jsx h */ import { h } from 'preact'; import { useAuth } from "./context.ts"; export default function Profile() { const { user, logout } = useAuth(); if (!user) { return <p>Please log in.</p>; } return ( <div> <p>Logged in as: {user}</p> <button onClick={logout}>Logout</button> </div> ); } """ **Why:** The Context API simplifies prop drilling and avoids passing props through intermediate components that don't need them. It's also useful for global state that is shared across the entire application. **Common Mistakes:** * Creating too many contexts. It's better to group related state into a single context than to create a separate context for each piece of state. * Not providing a default value for the context. This can lead to errors if a component tries to access the context outside of a provider. ### 3.4. Redux/Flux-like Architectures (with Deno) For complex applications, consider a Redux/Flux-like architecture, which offers a centralized store, predictable state updates, and unidirectional data flow. There are Deno-specific implementations and libraries built for this. **Do This:** * Define a clear and concise state schema for your application. * Create actions to represent state changes. * Implement pure reducers to handle actions and update the state immutably. * Use a middleware system to handle side effects (e.g., API calls). * Consider using libraries like "deno-redux" or "zustand" (though "zustand" does not follow the Redux pattern strictly, it provides a simplified central store). Evaluate if a full Redux pattern is needed, or if "zustand" is a better, less verbose solution. **Don't Do This:** * Mutate the state directly within reducers. * Perform side effects directly within reducers. * Create excessively complex reducers. Break them down into smaller, more manageable functions. **Example (Redux-like Architecture using "zustand"):** """typescript // store.ts import { create } from 'zustand'; interface BearState { bears: number; increase: (by: number) => void; } export const useBearStore = create<BearState>()((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })); // component.tsx (demonstrating usage - adapted for Deno/Fresh) import { useBearStore } from "./store.ts"; function MyComponent() { const bears = useBearStore((state) => state.bears); const increase = useBearStore((state) => state.increase); return ( <div> <p>Number of bears: {bears}</p> <button onClick={() => increase(1)}>Add a bear</button> </div> ); } """ **Why:** Redux-like architectures offer a predictable and manageable way to handle complex application state. They promote testability, maintainability, and scalability. **Common Mistakes:** * Overcomplicating the store setup and structure. * Not using TypeScript interfaces for state and actions, leading to type errors. * Ignoring the unidirectional data flow principle and causing unintended side effects. ### 3.5. Server-Side State Management Deno also offers the possibility to perform state management on the server. This is essential if you are working with user sessions, database interactions, or caching strategies. **Do This:** * Leverage Deno's built-in modules like "Deno.KV". "Deno.KV" provides a durable, low-latency key-value database. * Utilize caching strategies for frequently accessed data to minimize database queries. * Consider using libraries like Redis or Memcached for more advanced caching needs. * Implement proper security measures to protect sensitive data stored on the server. **Don't Do This:** * Expose sensitive server-side state directly to the client. * Store large amounts of data in memory without proper caching and garbage collection. * Ignore security vulnerabilities related to data storage and retrieval. **Example (Server-Side State Management with Deno.KV):** """typescript // server.ts const kv = await Deno.openKv(); async function incrementCounter(key: string) { const atomicOp = kv.atomic(); const counter = await kv.get([key]); const currentValue = counter.value || 0; atomicOp.set([key], currentValue + 1); const { ok } = await atomicOp.commit(); return ok; } // Example Usage incrementCounter("page_views") .then((success) => { if (success) { console.log("Counter incremented successfully."); } else { console.error("Failed to increment counter."); } }); // Get the key value const res = await kv.get(["page_views"]) console.log(res); //Close the KV store when done //await kv.close() """ **Why:** Server-side state management allows centralized and secure data storage, reducing client-side dependencies and improving performance by caching frequently accessed data. "Deno.KV" provides a built-in solution, simplifying setup and reducing external dependencies. **Common Mistakes:** * Storing sensitive data in plain text. * Not implementing proper error handling for database operations. * Ignoring potential race conditions when updating server-side state concurrently. ## 4. Advanced Patterns and Techniques ### 4.1. Functional Reactive Programming (FRP) FRP combines functional programming with reactive programming to create declarative and composable state management solutions. This can be accomplished via RxJS or similar libraries. **Do This:** * Use observables to represent streams of data over time. * Transform data streams using operators like "map", "filter", and "reduce". * Combine multiple data streams using operators like "merge", "combineLatest", and "concat". * Handle side effects using subscriptions. **Don't Do This:** * Create excessively complex observable chains that are difficult to understand and debug. * Ignore potential memory leaks by not unsubscribing from observables when they are no longer needed. ### 4.2. Event Sourcing Event sourcing involves persisting the state of an application as a sequence of events. The current state can be derived by replaying these events. This provides an audit trail and enables time-travel debugging. This approach often works very well with "Deno.KV" for persisting the events. **Do This:** * Define a clear and concise event schema for your application. * Store events in an append-only log (e.g., a database table). * Replay events to derive the current state. * Use snapshots to optimize the replay process for long event streams. **Don't Do This:** * Mutate existing events. * Store derived state directly in the event log. ## 5. Specific Considerations for Deno * **Permissions:** Deno's permission system should be explicitly considered when dealing with server-side state. Ensure the necessary permissions are granted for accessing databases, the file system, or other resources. * **Module Resolution:** Be mindful of Deno's module resolution when organizing state management logic. Use explicit imports and exports to clearly define dependencies and avoid naming conflicts. * **Top-Level Await:** Use top-level "await" with caution. Ensure that any asynchronous initializations of state or connections to data sources are handled correctly to avoid blocking the application's startup. * **ESM (ECMAScript Modules):** Deno is designed around ESM, so ensure your state management code is fully compatible and takes advantage of ESM's benefits. ## 6. Conclusion Choosing the right state management approach in Deno depends on the complexity of your application. Start with simple techniques for smaller projects and consider more robust solutions like Redux or FRP for larger, more complex applications. By following the principles and standards outlined in this document, you can ensure that your Deno applications are maintainable, performant, and secure. Always remember clarity, predictability, and immutability as cornerstones of effective state management.
# Performance Optimization Standards for Deno This document outlines coding standards for optimizing the performance of Deno applications. These standards are designed to improve speed, responsiveness, and resource utilization, leveraging Deno's unique features and the latest versions of the runtime. These standards are not just about writing code that works, but code that is efficient, scalable, and maintainable. ## 1. Architectural Considerations ### 1.1. Module Loading and Dependency Management **Standard:** Optimize module loading to reduce startup time and memory footprint. * **Do This:** * Use specific imports instead of wildcard imports to only load necessary modules. * Lazy load modules that are not immediately required at startup. * Leverage Deno's built-in dependency inspector to analyze module sizes and loading times ("deno info"). * **Don't Do This:** * Avoid wildcard imports ("import * as something from "./something.ts""). * Eagerly load all dependencies during application startup. * Over-rely on large, monolithic libraries when smaller, more focused alternatives exist. **Why:** Efficient module loading directly impacts application startup time and memory usage. Deno's decentralized module system requires careful management to prevent performance bottlenecks. **Example:** """typescript // Instead of: // import * as utils from "./utils.ts"; // utils.doSomething(); // utils.doSomethingElse(); // Do this: import { doSomething, doSomethingElse } from "./utils.ts"; doSomething(); doSomethingElse(); """ Lazy loading: """typescript async function loadModule() { const module = await import("./heavy_module.ts"); module.run(); } // Only load the module when needed document.getElementById("myButton").addEventListener("click", loadModule); """ ### 1.2 Caching Strategies **Importance:** Implementing effective caching mechanisms is critical for reducing latency and improving responsiveness, especially in web applications. **Standards:** * **Do This:** * Implement HTTP caching headers (e.g., "Cache-Control", "Expires", "ETag") to allow browsers and CDNs to cache static assets and API responses. * Use Deno's built-in "Deno.Kv" key-value store for caching frequently accessed data. * Utilize service workers to cache static assets and API responses for offline access and faster loading times. * **Don't Do This:** * Neglect caching static assets, leading to unnecessary server requests. * Over-cache dynamic content, resulting in stale data being displayed to users. * Store sensitive data in caches without proper encryption and security measures. **Why:** * *HTTP Caching:* Reduces server load and latency by allowing clients and intermediaries to cache responses. * *Deno.Kv Caching:* Provides fast and efficient access to cached data within your application. * *Service Worker Caching:* Enables offline access and significantly improves loading times for web applications. **Code Examples** Using Deno.Kv for caching: """typescript import { Kv } from "@deno/kv"; const kv = await new Kv(); async function getCachedData(key: string, fetcher: () => Promise<any>, ttl: number): Promise<any> { const cached = await kv.get([key]); if (cached.value) { console.log("Cache hit!"); return cached.value; } const data = await fetcher(); await kv.set([key], data, { expireIn: ttl }); console.log("Cache miss, fetching and caching."); return data; } async function fetchDataFromSource(): Promise<any> { // Simulate fetching data from an external source await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate network latency return { value: Math.random() }; } // Example usage: const data = await getCachedData("my_data", fetchDataFromSource, 60); // Cache for 60 seconds console.log(data); kv.close(); """ ### 1.3. Concurrency and Parallelism **Standard:** Utilize Deno's concurrency model effectively to maximize resource utilization. * **Do This:** * Use "async/await" and Promises for asynchronous operations to avoid blocking the main thread. * Leverage web workers for CPU-intensive tasks to offload work from the main event loop. * Utilize streams and iterators for processing large datasets without loading them entirely into memory. * **Don't Do This:** * Perform long-running, synchronous operations on the main thread. * Underutilize available CPU cores. * Load entire large files into memory at once. **Why:** Concurrency and parallelism are essential for building responsive and scalable applications. Deno's modern concurrency model enables developers to write efficient code that takes advantage of multiple CPU cores. **Example:** Web worker example: """typescript // main.ts const worker = new Worker(new URL("./worker.ts", import.meta.url).href, { type: "module" }); worker.postMessage("Start processing!"); worker.onmessage = (event) => { console.log("Received from worker:", event.data); }; // worker.ts self.onmessage = (event) => { console.log("Worker received:", event.data); // Simulate a CPU-intensive task let result = 0; for (let i = 0; i < 1000000000; i++) { result += i; } self.postMessage("Result: ${result}"); }; """ Stream example (copying a file): """typescript const file = await Deno.open("large_file.txt"); const dest = await Deno.create("copy_of_large_file.txt"); await Deno.copy(file, dest); file.close(); dest.close(); """ ### 1.4 Database Interactions **Standard:** Optimize database interactions to minimize latency and resource consumption. * **Do This:** * Use connection pooling to reuse database connections. * Optimize database queries with indexes. * Use prepared statements to prevent SQL injection and improve query performance. * Consider using an ORM or query builder for abstraction and security. * **Don't Do This:** * Create a new database connection for every request. * Run complex queries without proper indexing. * Construct SQL queries by concatenating strings directly (SQL injection risk). **Why:** Database interactions are often a bottleneck in web applications. Efficient database access patterns are crucial for minimizing latency and improving overall system performance. **Example:** Using connection pooling with "postgres": """typescript import { Pool } from "https://deno.land/x/postgres@v0.17.0/mod.ts"; const pool = new Pool({ hostname: "localhost", database: "mydb", user: "myuser", password: "mypassword", port: 5432, }, 10); // Limit to 10 connections const client = await pool.connect(); try { const result = await client.queryObject("SELECT * FROM users WHERE id = $1", [1]); console.log(result.rows); } finally { client.release(); } // await pool.end(); // Close the pool when done """ ## 2. Code-Level Optimizations ### 2.1. Data Structures and Algorithms **Standard:** Choose appropriate data structures and algorithms for the task at hand. * **Do This:** * Use "Map" and "Set" for efficient lookups and uniqueness checks. * Use Typed Arrays ("Uint8Array", "Float64Array", etc.) when working with binary data. * Choose algorithms with appropriate time complexity for the expected input size (e.g., use efficient sorting algorithms for large datasets). * **Don't Do This:** * Use arrays for lookups when "Map" or "Set" would be more efficient. * Perform unnecessary iterations or computations. * Ignore the performance characteristics of different data structures and algorithms. **Why:** The choice of data structures and algorithms has a significant impact on the performance of your code, especially when dealing with large datasets or computationally intensive tasks. **Example:** Using "Set" for uniqueness checking: """typescript const myArray = [1, 2, 2, 3, 4, 4, 5]; const uniqueValues = [...new Set(myArray)]; console.log(uniqueValues); // [1, 2, 3, 4, 5] """ Using "Uint8Array" for binary data: """typescript const buffer = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello" in ASCII const decoder = new TextDecoder(); const text = decoder.decode(buffer); console.log(text); // Hello """ ### 2.2. String Manipulation **Standard:** Optimize string manipulation routines to reduce memory allocation and improve speed. * **Do This:** * Use template literals for string concatenation instead of the "+" operator. * Use "String.prototype.startsWith", "String.prototype.endsWith", and "String.prototype.includes" instead of regular expressions for simple checks. * Avoid unnecessary string copies. * **Don't Do This:** * Perform excessive string concatenation using the "+" operator in loops. * Use regular expressions when simpler string methods suffice. * Create unnecessary intermediate strings. **Why:** String operations can be surprisingly expensive, especially in performance-critical sections of your code. Optimizing string manipulation can lead to significant performance improvements. **Example:** Using template literals: """typescript const name = "World"; const greeting = "Hello, ${name}!"; console.log(greeting); // Hello, World! """ Avoiding unnecessary string copies: """typescript // Instead of // let str = ""; // for (let i = 0; i < 1000; i++) { // str += "a"; // creates a new string each time // } // Do This: const arr = new Array(1000).fill("a"); const str = arr.join(""); // Creates one string at the end. """ ### 2.3. Object and Array Operations **Standard:** Optimize object and array operations regarding immutability, cloning, and modification. * **Do This:** * Use immutable data structures where appropriate to avoid unintended side effects. * Use the spread operator ("...") or "Array.from()" for shallow copying of objects or arrays. * Use in-place array modification methods when possible. * **Don't Do This:** * Mutate objects and arrays directly when immutability is desired. * Use deep cloning for simple object copies. * Create unnecessary intermediate arrays. **Why:** Efficient object and array manipulation is crucial for data processing-intensive applications. Immutability can improve code clarity and prevent bugs, while efficient copying techniques can reduce memory allocation and improve performance. **Example:** Shallow copying using the spread operator: """typescript const originalArray = [1, 2, 3]; const newArray = [...originalArray]; // Creates a shallow copy newArray[0] = 4; // Modifying newArray does not affect originalArray console.log(originalArray); // [1, 2, 3] console.log(newArray); // [4, 2, 3] """ Using immutability with "Object.freeze()": """typescript const immutableObject = Object.freeze({ x: 1, y: 2 }); // immutableObject.x = 3; // This will throw an error in strict mode """ ### 2.4 Regular Expressions **Standard:** Write efficient regular expressions and avoid unnecessary use of regular expressions. * **Do This:** * Use non-capturing groups "(?:...)" where capturing is not required. * Compile regular expressions that are used repeatedly. * Prefer character classes ("[a-z]") to alternation ("a|b|c") where possible. * **Don't Do This:** * Use overly complex regular expressions when simpler alternatives exist. * Create new regular expressions in loops. * Use backtracking excessively. **Why:** Regular expressions can be powerful but also computationally expensive. Writing efficient regular expressions and avoiding their unnecessary use is crucial for performance. **Example:** Compiling a regular expression: """typescript const regex = /^hello/i; regex.test("Hello world"); """ Using non-capturing groups: """typescript const regex = /^(?:https?:\/\/)?([^\/]+)/; // Only capture the domain name const url = "https://www.example.com/path"; const match = regex.exec(url); console.log(match?.[1]); // www.example.com """ ## 3. Deno-Specific Optimizations ### 3.1. Deno Standard Library Usage **Standard:** Prefer Deno's standard library for common tasks whenever possible. * **Do This:** * Use "Deno.readFile" and "Deno.writeFile" for file I/O. * Use "Deno.serve" for creating HTTP servers unless more complex routing is required. * Use the "std/hash" module. * **Don't Do This:** * Import third-party libraries for simple tasks that can be accomplished with the standard library. * Reinvent the wheel. **Why:** Deno's standard library is designed to be efficient and secure. Using it avoids unnecessary dependencies and reduces the risk of security vulnerabilities. It also ensures tighter integration with the Deno runtime, potentially leading to better performance. **Example:** Using "Deno.serve" for a simple HTTP server: """typescript import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; const handler = (request: Request): Response => { const body = "Your user-agent is:\n\n${request.headers.get("user-agent") ?? "Unknown"}"; return new Response(body, { status: 200 }); }; console.log("Listening on http://localhost:8000"); serve(handler, { port: 8000 }); """ ### 3.2. Permissions Management **Standard:** Request only the necessary permissions for your application. * **Do This:** * Use the "--allow-read", "--allow-write", "--allow-net", "--allow-env", etc., flags to grant specific permissions. * Avoid using "--allow-all" in production environments. * Review and minimize the required permissions regularly. * **Don't Do This:** * Grant unnecessary permissions to your application. * Run your application with "--allow-all" without understanding the security implications. **Why:** Deno's security model relies on explicit permissions. Requesting only the necessary permissions reduces the attack surface and improves the security of your application. It can indirectly impact performance by reducing the overhead of permission checks. **Example:** Running a script with specific permissions: """bash deno run --allow-read=./data.txt --allow-net=example.com main.ts """ ### 3.3. Transpilation and Bundling **Standard:** Optimize transpilation and bundling processes for production deployments. * **Do This:** * Use "deno compile" to create a single, optimized executable. * Use a bundler like "esbuild" or "rollup" when more control over the bundling process is required. * Minify and compress your code for production deployments. * **Don't Do This:** * Deploy unbundled code to production. * Skip minification and compression. **Why:** Transpilation and bundling can significantly improve the performance and deployment characteristics of your Deno applications. Bundling reduces the number of network requests, while minification and compression reduce the size of the code. **Example:** Compiling a Deno application: """bash deno compile --output myapp main.ts """ Using "esbuild": """javascript // build.js import * as esbuild from "https://deno.land/x/esbuild@v0.17.19/mod.js"; await esbuild.build({ entryPoints: ["./main.ts"], bundle: true, outfile: "./dist/main.js", format: "esm", minify: true, platform: "browser", }); esbuild.stop(); """ ### 3.4. Profiling and Performance Monitoring **Standard:** Use profiling and performance monitoring tools to identify bottlenecks and optimize code. * **Do This:** * The Deno runtime includes a built-in profiler which can be enabled via the "--prof" flag. * Use appropriate logging levels. * **Don't Do This:** * Guess at performance issues. * Ignore performance metrics. **Why:** Regular profiling and performance monitoring are essential for identifying and addressing performance bottlenecks in your Deno applications. These tools provide valuable insights into the runtime behavior of your code, enabling you to make informed optimization decisions. **Example:** To generate such a profiling report, run your program with the --prof command line flag as follows: """shell deno run --allow-read --allow-write --prof main.ts """ Once the program exits a "deno_prof.json" file will be written in the current working directory. ## 4. Error Handling **Standard** Implement comprehensive and efficient error handling to prevent performance degradation caused by exceptions. * **Do This:** * Use "try...catch" blocks to handle potential exceptions gracefully. * Implement custom error types for better error identification and handling. * Use "finally" blocks to ensure resources are released even if an error occurs. * **Don't Do This:** * Ignore potential errors, leading to unhandled exceptions and crashes. * Rely solely on generic "catch" blocks without specific error handling. * Throw and catch exceptions for control flow, as it is expensive. **Why:** Proper error handling prevents abrupt program termination and resource leaks, ensuring stability and performance. Custom error types improve debuggability and allow for targeted error handling. **Example:** """typescript async function readFile(filename: string): Promise<string | undefined> { try { const data = await Deno.readTextFile(filename); return data; } catch (error) { if (error instanceof Deno.errors.NotFound) { console.error("File not found: ${filename}"); return undefined; } else { console.error("Error reading file ${filename}: ${error.message}"); throw error; // Re-throw the error if it's not a NotFound error } } finally { console.log("File reading process completed (or attempted)."); } } // Usage readFile("my_file.txt") .then(content => { if (content) { console.log("File content: ${content}"); } }); """ ### 5. Code Reviews and Continuous Improvement **Standard:** Incorporate performance considerations into code reviews and continuously improve code based on performance metrics. * **Do This:** * Include performance as a key criterion in code reviews. * Regularly profile and analyze code for potential performance improvements. * Track performance metrics over time to identify regressions and areas for optimization. * **Don't Do This:** * Ignore performance considerations during code reviews. * Treat performance optimization as a one-time task. * Fail to monitor and track performance metrics. **Why:** Continuous attention to performance is crucial for maintaining a high-quality, efficient codebase. Code reviews and performance monitoring provide valuable opportunities to identify and address potential performance issues early in the development lifecycle.
# Testing Methodologies Standards for Deno This document outlines the recommended testing methodologies for Deno projects. Adhering to these standards ensures code quality, maintainability, reliability, and security. It covers unit, integration, and end-to-end testing strategies specifically tailored for the Deno environment. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) Encouraged * **Do This:** Embrace TDD where appropriate. Write tests *before* implementing features. This helps clarify requirements and ensures testable code from the start. * **Don't Do This:** Write tests as an afterthought or skip tests altogether. This leads to fragile code that is difficult to refactor and prone to errors. * **Why:** TDD promotes a clear understanding of requirements, improves code design, and reduces defects. * **Example:** """typescript // src/calculator.ts export function add(a: number, b: number): number { return a + b; } // test/calculator_test.ts import { add } from "../src/calculator.ts"; import { assertEquals } from "https://deno.land/std@0.215.0/assert/mod.ts"; Deno.test("add() returns the sum of two numbers", () => { assertEquals(add(2, 3), 5); }); """ ### 1.2. Comprehensive Test Coverage * **Do This:** Aim for high test coverage (ideally above 80%). Focus on testing critical paths, edge cases, and error handling. Use Deno's built-in code coverage tools. * **Don't Do This:** Rely solely on happy-path tests. Neglecting error handling and edge cases leaves your application vulnerable. * **Why:** High test coverage reduces the risk of regressions and ensures that changes don't introduce unexpected behavior. * **Example:** """bash deno test --coverage ./test # Run tests and generate coverage data deno coverage ./test/coverage # Display CLI coverage report """ ### 1.3. Clear and Concise Tests * **Do This:** Write tests that are easy to understand and maintain. Each test should focus on a single, well-defined aspect of the code. Use descriptive test names. Consider using "describe" blocks to group related tests. * **Don't Do This:** Write overly complex or brittle tests that are difficult to understand or that break easily with minor code changes. * **Why:** Clear tests make it easier to identify and fix bugs and to refactor code with confidence. * **Example:** """typescript import { assertEquals } from "https://deno.land/std@0.215.0/assert/mod.ts"; Deno.test("User registration", async (t) => { await t.step("should register a new user", () => { // Test logic for user registration }); await t.step("should return an error if the email already exists", () => { // Test logic for duplicate email error }); await t.step("should validate password strength", () => { // Test logic for password validation }); }); """ ### 1.4. Isolation and Mocking * **Do This:** Isolate units of code during testing to prevent dependencies from interfering with test results. Use mocking or stubbing to replace external dependencies with controlled substitutes. Deno provides built-in support for mocking via the "Deno.test" API. * **Don't Do This:** Test units of code with real dependencies whenever possible. This introduces flakiness and makes it difficult to pinpoint the source of errors. * **Why:** Isolation ensures that tests are reproducible and that you are testing the intended unit of code in isolation. * **Example:** """typescript import { assertEquals } from "https://deno.land/std@0.215.0/assert/mod.ts"; import { stub } from "https://deno.land/std@0.215.0/testing/mock.ts"; // Some service that fetches data const fetchData = async () => { const response = await fetch("https://api.example.com/data"); return await response.json(); }; Deno.test("fetchData() retrieves data from API", async () => { // Stub the fetch function to return mock data const stubFetch = stub( globalThis, "fetch", () => Promise.resolve(new Response(JSON.stringify({ message: "Mock data" }))) ); const data = await fetchData(); assertEquals(data, { message: "Mock data" }); stubFetch.restore(); // Restore original fetch }); """ ### 1.5. Avoid Test Logic in Production Code * **Do This:** Keep test-specific logic separate from production code. Use dependency injection or feature flags to provide test hooks. * **Don't Do This:** Embed test-specific code directly in production code. * **Why:** Separating test concerns from production code keeps your application clean and maintainable. ### 1.6. Data Fixtures and Test Data Management * **Do This:** Use data fixtures to provide consistent and predictable data for your tests. Employ strategies for managing test data, such as database seeding or in-memory data stores. Consider using a utility library for generating realistic, synthetic data. * **Don't Do This:** Hardcode data values directly in tests. Use production databases for testing (unless specifically required for end-to-end testing, and then use a dedicated testing environment). * **Why:** Well-managed test data makes tests more reliable, repeatable, and easier to understand. ## 2. Unit Testing ### 2.1. Focus * **Do This:** Unit tests should focus on verifying the behavior of individual functions, classes, or modules in isolation. Test only the public API of a unit. * **Don't Do This:** Test internal implementation details that are subject to change. This leads to brittle tests that are difficult to maintain. Write integration tests as unit tests. * **Why:** Unit tests provide fast feedback and help you identify bugs early in the development process. ### 2.2. Tooling * **Do This:** Use Deno's built-in "Deno.test" API and assertion functions (e.g., "assertEquals", "assertThrows") for writing unit tests. Consider using third-party assertion libraries for more advanced features or specialized assertions. * **Don't Do This:** Rely solely on "console.log" for verifying test results. * **Example:** """typescript import { assertEquals, assertThrows } from "https://deno.land/std@0.215.0/assert/mod.ts"; function divide(a: number, b: number): number { if (b === 0) { throw new Error("Cannot divide by zero"); } return a / b; } Deno.test("divide() returns the correct quotient", () => { assertEquals(divide(10, 2), 5); }); Deno.test("divide() throws an error when dividing by zero", () => { assertThrows(() => divide(10, 0), Error, "Cannot divide by zero"); }); """ ### 2.3. Mocking Strategies * **Do This:** Use mocking to isolate units of code from external dependencies, such as databases, APIs, or file systems. Deno's built-in testing API allows for comprehensive mocking. Consider using libraries designed for mocking such as "sinon". * **Don't Do This:** Mock indiscriminately. Only mock dependencies that are necessary to isolate the unit under test. * **Why:** Mocking makes unit tests faster, more reliable, and easier to control. * **Example:** """typescript // A service that relies on fetching data... const getDataFromService = async (id: string) => { const resp = await fetch("https://example.com/api/data/${id}"); // External API call return await resp.json(); } Deno.test("getDataFromService() should fetch and return data", async () => { const mockResponse = {success: true, data: {id: '123', name: 'Test Data'}}; const fetchStub = stub( globalThis, "fetch", () => Promise.resolve(new Response(JSON.stringify(mockResponse))) ); const result = await getDataFromService("123"); assertEquals(result, mockResponse); fetchStub.restore(); }); """ ### 2.4. Testing Error Handling * **Do This:** Test that your code handles errors gracefully and provides informative error messages. Use "assertThrows" to verify that exceptions are thrown under the correct conditions. * **Don't Do This:** Ignore error handling in your tests. ### 2.5 Asynchronous Code Testing * **Do This:** When writing unit tests for asynchronous code, ensure proper handling of assertions within asynchronous operations. Use "await" when necessary to ensure the tests wait for the asynchronous operation to complete before making assertions. * **Don't Do This:** Forget the "await" keyword or async testing will be unreliable. ## 3. Integration Testing ### 3.1. Focus * **Do This:** Integration tests should verify the interaction between two or more units of code or between your code and external systems (e.g., databases, APIs). * **Don't Do This:** Test the internal details of individual units. Write integration tests that are too broad or that test the entire application. * **Why:** Integration tests ensure that the different parts of your application work together correctly. ### 3.2. Database Interactions * **Do This:** Use a dedicated test database for integration tests. Seed the database with data fixtures before running tests and clean it up afterward. Use transactions to rollback changes made during tests. * **Don't Do This:** Use a production database for integration testing. ### 3.3. API Interactions * **Do This:** Mock external APIs or use a test API endpoint for integration tests. Verify that your code sends the correct requests and handles the responses correctly. * **Don't Do This:** Rely on real external APIs for integration tests. ### 3.4. Example: Testing Database Interactions """typescript import { assertEquals } from "https://deno.land/std@0.215.0/assert/mod.ts"; import { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts"; // Or other DB Client const dbConfig = { user: "testuser", database: "testdb", hostname: "localhost", port: 5432, password: "testpassword" }; const client = new Client(dbConfig); Deno.test("Integration: Database User Creation", async () => { await client.connect(); try { await client.queryArray("BEGIN"); // Start transaction const result = await client.queryArray("INSERT INTO users (name, email) VALUES ('Test User', 'test@example.com') RETURNING id"); const userId = result.rows[0][0]; const checkResult = await client.queryArray("SELECT name FROM users WHERE id = $1", [userId]); assertEquals(checkResult.rows[0][0], "Test User"); } finally { await client.queryArray("ROLLBACK"); // Rollback after test. await client.end(); } }); """ ### 3.5 Using Deno KV for Testing Deno KV is useful for simple integration tests. It's easy to setup locally, but do not put sensitive information into KV storage during testing. ## 4. End-to-End (E2E) Testing ### 4.1. Focus * **Do This:** E2E tests should verify the entire application flow, from the user interface to the database. Simulate real user interactions with the application. * **Don't Do This:** Test individual components of the application. Write E2E tests that are too granular. * **Why:** E2E tests ensure that the application works correctly from the user's perspective. ### 4.2. Tooling * **Do This:** Use a testing framework like Playwright, Puppeteer or Cypress for writing E2E tests. These frameworks provide APIs for controlling browsers and simulating user interactions. Deno libraries exist providing convenient wrappers/functionality around these frameworks. * **Don't Do This:** Manually test the application for every change. ### 4.3. Test Environment * **Do This:** Deploy the application to a dedicated test environment for running E2E tests. Use a realistic data set for testing. * **Don't Do This:** Run E2E tests in a production environment. ### 4.4 Considerations * **State Management**: Testing state transitions are crucial in end-to-end scenarios. Clean up state between tests or "afterEach". * **Visual Regression Testing**: Consider capturing screenshots and comparing them across builds to detect UI changes. * **Performance**: E2E tests are time intensive. Run them in parallel where possible and profile slow performing tests. ### 4.5 Example using Playwright First, install playwright: "deno run -A npm:playwright install" Then create a test file: """typescript import { assertEquals } from "https://deno.land/std@0.215.0/assert/assert_equals.ts"; import { chromium } from "npm:playwright"; Deno.test("End-to-end test: Navigating and interacting with a page", async () => { const browser = await chromium.launch(); const page = await browser.newPage(); try { await page.goto("https://example.com"); assertEquals(await page.title(), "Example Domain"); // Assert title // Taking a screenshot for visual verification await page.screenshot({ path: "example.png" }); } finally { await browser.close(); } }); """ ## 5. Deno-Specific Considerations ### 5.1. Permissions * **Do This:** Test the effects of different permission settings on your code. Use "--allow-read", "--allow-write", "--allow-net", etc. to simulate different access levels. * **Don't Do This:** Assume that your code will always have all permissions. * **Example:** """typescript // src/file_reader.ts export async function readFile(path: string): Promise<string> { try { const data = await Deno.readTextFile(path); return data; } catch (error) { console.error("Error reading file: ${error}"); return ""; } } // test/file_reader_test.ts import { readFile } from "../src/file_reader.ts"; import { assertEquals } from "https://deno.land/std@0.215.0/assert/mod.ts"; Deno.test("readFile() reads a file with --allow-read permission", async () => { const content = await readFile("./test_file.txt"); assertEquals(content, "Test content"); }); Deno.test({ name: "readFile() fails to read a file without --allow-read permission", permissions: { read: [] }, fn: async () => { const content = await readFile("./test_file.txt"); assertEquals(content, ""); // Expect an error and empty content }, }); // test_file.txt (in same directory) // Test content """ ### 5.2. ES Modules and Imports * **Do This:** Use explicit import specifiers for all modules. Test that your code correctly imports and exports modules. Follow best practices for module resolution. * **Don't Do This:** Rely on implicit module resolution or magic imports. ### 5.3. Web Standard APIs * **Do This:** When using web standard APIs (e.g., "fetch", "WebSocket", "Web Storage"), test that your code interacts with these APIs correctly. Mock these APIs as needed for unit testing. Deno encourages the use of native web APIs, making tests somewhat transferable between environments, especially when using mocking libraries. ## 6. Performance Testing ### 6.1 Benchmarking * **Do This:** Use "Deno.bench()" to benchmark critical code paths. This helps to identify performance bottlenecks and ensure that changes don't introduce performance regressions. * **Don't Do This:** Ignore the performance of your code. * **Example:** """typescript Deno.bench("fast function", () => { // Code to benchmark }); """ ### 6.2 Load Testing * **Do This:** Simulate realistic user load to measure the performance and scalability of your application. Common tools include "wrk" and "hey". * **Don't Do This:** Deploy without understanding maximum requests and connections. ## 7. Continuous Integration (CI) ### 7.1. Automated Testing * **Do This:** Integrate your tests into a CI pipeline to automatically run tests on every code change. Use tools like GitHub Actions, GitLab CI, or Travis CI. * **Don't Do This:** Merged untested code. ### 7.2. Code Coverage Reporting * **Do This:** Integrate code coverage reporting into your CI pipeline to track test coverage over time. ## 8. Security Testing ### 8.1 Static Analysis * **Do This:** Use static analysis tools to identify potential security vulnerabilities in your code. These should run in CI pipelines. ### 8.2. Dependency Scanning * **Do This:** Regularly scan your dependencies for known vulnerabilities with tools like "deno audit". ## 9. Documentation Testing ### 9.1 Example Validation * **Do This:** Embed runnable code examples in your documentation and use documentation testing tools to automatically verify that these examples work correctly. """text """ts console.log("Hello, Deno!"); """ """ ## 10. Review and Refinement These coding standards for testing will be reviewed, at minimum, on a quarterly basis. The document will be refined based on feedback from the development team, latest Deno features, security vulnerabilities, and advances in industry best practices.