# API Integration Standards for Deno
This document outlines the standards for integrating with backend services and external APIs in Deno. These standards prioritize maintainability, performance, and security, ensuring consistent and high-quality code across projects. The primary focus is on leveraging modern Deno features and best practices.
## 1. Architectural Considerations
### 1.1 Separation of Concerns
**Do This:**
* Separate API integration logic from core business logic. Create dedicated modules or services responsible for handling API communication. This enhances testability and reduces coupling.
**Don't Do This:**
* Embed API calls directly within application logic. This makes the code harder to test, understand, and maintain.
**Why:**
* Promotes modularity, testability, and reduces the impact of API changes on the core application.
**Example:**
"""typescript
// Good: Separate API client
// api_client.ts
export async function fetchUserData(userId: string): Promise {
const response = await fetch("https://api.example.com/users/${userId}");
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
}
// user_service.ts
import { fetchUserData } from "./api_client.ts";
export async function getUser(userId: string): Promise {
try {
const userData = await fetchUserData(userId);
return userData;
} catch (error) {
console.error("Failed to fetch user data:", error);
throw error; // Re-throw or handle appropriately
}
}
// Bad: API call directly in the service
// user_service.ts
export async function getUser(userId: string): Promise {
try {
const response = await fetch("https://api.example.com/users/${userId}");
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
} catch (error) {
console.error("Failed to fetch user data:", error);
throw error; // Re-throw or handle appropriately
}
}
"""
### 1.2 API Abstraction
**Do This:**
* Create an abstraction layer between your application and the external API. This could be a dedicated class or module encompassing API-related functions.
**Don't Do This:**
* Directly expose external API interfaces to the rest of the application
**Why:**
* Isolates the application from direct reliance on the external API's structure or interface. Simplifies mocking for testing. Enables future swaps to a different service with minimal code change.
**Example:**
"""typescript
// api_abstraction.ts
interface User {
id: string;
name: string;
email: string;
}
interface APIClient {
getUser(id: string): Promise;
}
class ConcreteAPIClient implements APIClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async getUser(id: string): Promise {
const resp = await fetch("${this.baseUrl}/users/${id}");
if (!resp.ok) {
throw new Error("HTTP error! status: ${resp.status}");
}
return await resp.json();
}
}
export { APIClient, ConcreteAPIClient, User };
// Main application code interacts with the APIClient interface
import { APIClient, ConcreteAPIClient, User } from "./api_abstraction.ts";
// Instantiate the client using a concrete client implementation
const apiClient: APIClient = new ConcreteAPIClient("https://api.example.com");
async function main(userId: string) {
try {
const user: User = await apiClient.getUser(userId);
console.log("User ID: ${user.id}, Name: ${user.name}, Email: ${user.email}");
} catch (e) {
console.error("Failed to fetch user with id ${userId}: ${e}");
}
}
// Example call
main("123");
"""
### 1.3 Configuration Management
**Do This:**
* Use environment variables or configuration files to manage API keys, endpoints, and other sensitive information. Use Deno's built-in "Deno.env" or a dedicated config library like "std/dotenv".
**Don't Do This:**
* Hardcode API keys or endpoints directly into the code.
**Why:**
* Enhances security, simplifies deployments across different environments, and promotes code reusability.
**Example:**
"""typescript
// .env file:
API_KEY=your_api_key
API_ENDPOINT=https://api.example.com
// main.ts
import { load } from "https://deno.land/std@0.218.2/dotenv/mod.ts";
const env = await load();
const apiKey = env["API_KEY"] || Deno.env.get("API_KEY"); // Prioritize .env file
const apiEndpoint = env["API_ENDPOINT"] || Deno.env.get("API_ENDPOINT");
if (!apiKey || !apiEndpoint) {
console.error("API_KEY or API_ENDPOINT not found in environment.");
Deno.exit(1);
}
async function fetchData() {
const response = await fetch("${apiEndpoint}/data", {
headers: {
"X-API-Key": apiKey,
},
});
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const data = await response.json();
console.log(data);
}
fetchData();
"""
## 2. Implementation Standards
### 2.1 HTTP Client
**Do This:**
* Use Deno's built-in "fetch" API for making HTTP requests.
**Don't Do This:**
* Rely on outdated or third-party HTTP client libraries unless they provide specific features not available in "fetch".
**Why:**
* "fetch" is a modern, standardized API that's natively supported by Deno. It eliminates external dependencies and provides a clean and straightforward interface. Prioritize standard library over external dependencies unless the stdlib does not fulfill your needs.
**Example:**
"""typescript
async function fetchData(url: string): Promise {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
} catch (error) {
console.error("Fetch error:", error);
throw error; // Re-throw or handle appropriately
}
}
// Usage:
fetchData("https://api.example.com/data")
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
"""
### 2.2 Error Handling
**Do This:**
* Implement robust error handling for API requests. Check the HTTP status code, handle network errors, and provide informative error messages.
**Don't Do This:**
* Ignore errors or provide generic error messages that provide no context.
**Why:**
* Ensures the application can gracefully handle unexpected API responses or network issues. Improves debugging and troubleshooting.
**Example:**
"""typescript
async function fetchData(url: string): Promise {
try {
const response = await fetch(url);
if (response.status === 404) {
throw new Error("Resource not found.");
}
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
} catch (error) {
console.error("Fetch error:", error);
throw error; // Re-throw or handle appropriately
}
}
"""
### 2.3 Data Validation
**Do This:**
* Validate the data received from external APIs. Use schema validation libraries like "zod", "valibot", or "yup" or write custom validation functions.
**Don't Do This:**
* Assume the data received from the API is always in the correct format.
**Why:**
* Protects the application from malformed or unexpected data, preventing crashes or incorrect behavior.
**Example:**
"""typescript
import { z } from "https://deno.land/x/zod@v3.23.6/mod.ts";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
age: z.number().min(0).max(120).optional(),
});
type User = z.infer;
async function fetchAndValidateUser(url: string): Promise {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const data = await response.json();
try {
const validatedData = UserSchema.parse(data);
return validatedData;
} catch (error: any) {
console.error("Validation error:", error.errors);
throw new Error("Invalid user data received from API.");
}
}
// Usage:
fetchAndValidateUser("https://api.example.com/users/123")
.then((user) => console.log("Validated user:", user))
.catch((error) => console.error("Error:", error));
"""
### 2.4 Rate Limiting & Retries
**Do This:**
* Implement rate limiting to prevent overwhelming external APIs.
* Implement retry mechanisms with exponential backoff for handling transient errors. Use libraries designed for retries, or implement custom logic.
**Don't Do This:**
* Make uncontrolled API requests without considering rate limits or potential errors.
**Why:**
* Ensures your application respects API usage limits and can recover from temporary network issues gracefully. Avoids being blocked or penalized by the API provider.
**Example:**
"""typescript
import { delay } from "https://deno.land/std@0.218.2/async/delay.ts";
const MAX_RETRIES = 3;
const INITIAL_DELAY = 1000; // 1 second
async function fetchDataWithRetries(url: string, retryCount = 0): Promise {
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 429 && retryCount < MAX_RETRIES) { // Rate Limited
const retryAfter = response.headers.get("Retry-After"); // In seconds
const delayTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : INITIAL_DELAY * Math.pow(2, retryCount);
console.warn("Rate limited. Retrying in ${delayTime / 1000} seconds...");
await delay(delayTime); // Wait before retrying
return fetchDataWithRetries(url, retryCount + 1); // Recursive call
}
throw new Error("HTTP error! Status: ${response.status}");
}
return await response.json();
} catch (error) {
if (retryCount < MAX_RETRIES) {
const delayTime = INITIAL_DELAY * Math.pow(2, retryCount);
console.warn("Error fetching data, retrying in ${delayTime / 1000} seconds...");
await delay(delayTime);
return fetchDataWithRetries(url, retryCount + 1);
}
console.error("Max retries reached. Failed to fetch data:", error);
throw error; // Re-throw after max retries
}
}
// Usage:
fetchDataWithRetries("https://api.example.com/data")
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
"""
### 2.5 Data Transformation
**Do This:**
* Transform data received from APIs into a format that is suitable for your application's internal use. Centralized data transformation.
**Don't Do This:**
* Use API data directly without any transformation, especially if the API data structure differs from your application's data model.
**Why:**
* Decouples your application from the API's specific data structure, making it easier to adapt to changes in the API or switch to a different API provider. Promotes a consistent data model in the internal application layers.
**Example:**
"""typescript
interface ApiResponse {
user_id: string;
full_name: string;
email_address: string;
}
interface User {
id: string;
name: string;
email: string;
}
function transformUserData(apiResponse: ApiResponse): User {
return {
id: apiResponse.user_id,
name: apiResponse.full_name,
email: apiResponse.email_address,
};
}
async function fetchAndTransformUser(url: string): Promise {
const response = await fetch(url);
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const apiData: ApiResponse = await response.json();
return transformUserData(apiData);
}
// Usage:
fetchAndTransformUser("https://api.example.com/users/123")
.then((user) => console.log("Transformed user:", user))
.catch((error) => console.error("Error:", error));
"""
### 2.6 Authentication and Authorization
**Do This:**
* Use secure authentication and authorization mechanisms, such as API keys, OAuth 2.0, or JWTs (JSON Web Tokens). Store secrets securely using Deno's permissions model (e.g., not committing keys to the repo).
**Don't Do This:**
* Hardcode sensitive credentials directly in the code or store them in insecure locations.
**Why:**
* Protects your application and the API from unauthorized access.
**Example (API Key):**
"""typescript
// See previous example in 1.3 Configuration Management for API Key handling:
// Using environment variables for storing the API key
// Secure coding practices involves more than just storing the key like protecting the endpoint
import { load } from "https://deno.land/std@0.218.2/dotenv/mod.ts";
const env = await load();
const apiKey = env["API_KEY"] || Deno.env.get("API_KEY"); // Prioritize .env file
if (!apiKey) {
console.error("API_KEY not found in environment.");
Deno.exit(1);
}
async function fetchData() {
const response = await fetch("https://api.example.com/data", {
headers: {
"X-API-Key": apiKey,
},
});
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const data = await response.json();
console.log(data);
}
fetchData();
"""
**Example (OAuth 2.0):**
"""typescript
// This example outlines the general flow. Refer to a specific OAuth 2.0 library
// for complete and secure implementations. Deno does not have a de-facto OAuth 2.0 package in the standard library
import {
OAuth2Client,
AuthorizationCodeGrant,
} from "https://deno.land/x/oauth2_client@v1.0.2/mod.ts";
const client = new OAuth2Client({
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
authorizationEndpointUri: "https://example.com/oauth2/authorize",
tokenUri: "https://example.com/oauth2/token",
redirectUri: "http://localhost:8000/callback",
scope: "read:profile write:data",
});
// 1. Redirect user to authorization endpoint
async function redirectToAuth() {
const authorizationUri = await client.code.getAuthorizationUri();
console.log("Redirect to:", authorizationUri);
// In a real application, this would be a Deno server redirect
}
// 2. Handle the callback after user authorizes the app.
async function handleCallback(request: Request) {
const url = new URL(request.url)
const code = url.searchParams.get("code");
if (!code) {
return new Response("Missing authorization code", { status: 400 });
}
try {
const tokens = await client.code.getToken(request.url); // Exchange code for tokens
// Now you can use the access token to make API calls
console.log("Access Token:", tokens.accessToken);
console.log("Refresh Token:", tokens.refreshToken);
// In a real app store these securely (e.g., in a database associated with user)
return new Response("Authentication successful! Check console for tokens.");
} catch (error) {
console.error("Failed to get token:", error);
return new Response("Authentication failed", {status: 500});
}
}
// Example simple Deno server to illustrate the flow
import { serve } from "https://deno.land/std@0.218.2/http/server.ts";
async function handler(request: Request): Promise {
const url = new URL(request.url);
if (url.pathname === '/auth') {
await redirectToAuth(); // This function doesn't return a Response. In real apps the function would trigger a redirect.
return new Response("Redirecting for Auth..."); // Replace with actual redirect
} else if (url.pathname === '/callback') {
return await handleCallback(request);
}
return new Response("Hello, Deno!");
}
console.log("Server listening on http://localhost:8000");
serve(handler, { port: 8000 });
"""
### 2.7 Caching
**Do This:**
* Implement caching mechanisms (e.g., in-memory, Redis, or Deno KV) when appropriate to reduce API request frequency and improve performance.
**Don't Do This:**
* Cache sensitive data without proper encryption or security measures. Cache data aggressively without considering its validity or expiration.
**Why:**
* Reduces latency, lowers API costs, and improves application responsiveness.
**Example (Deno KV):**
"""typescript
const kv = await Deno.openKv();
async function fetchUserData(userId: string): Promise {
const cacheKey = ["user", userId];
const cachedData = await kv.get(cacheKey);
if (cachedData.value) {
console.log("Serving data from cache");
return cachedData.value;
}
console.log("Fetching data from API");
const response = await fetch("https://api.example.com/users/${userId}");
if (!response.ok) {
throw new Error("HTTP error! Status: ${response.status}");
}
const userData = await response.json();
// Store the data in KV with a 1 hour expiration
await kv.set(cacheKey, userData, { expireIn: 3600 }); // 3600 seconds = 1 hour
return userData;
}
// Usage:
fetchUserData("123")
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
"""
## 3. Security Considerations
### 3.1 Input Sanitization
**Do This:**
* Sanitize any user-provided input before sending it to external APIs to prevent injection attacks.
**Don't Do This:**
* Pass unsanitized user input directly to APIs.
**Why:**
* Protect against vulnerabilities like cross-site scripting (XSS) or command injection.
### 3.2 HTTPS
**Do This:**
* Always communicate with APIs over HTTPS to encrypt data in transit.
**Don't Do This:**
* Use HTTP for sensitive API communication. Don't disable TLS verification unless absolutely necessary and with strong justification.
**Why:**
* Protects data from eavesdropping and tampering.
### 3.3 Secret Management
**Do This:**
* Use environment variables or dedicated secret management tools to store API keys, passwords, and other sensitive information. As discussed earlier, avoid hardcoding secrets.
**Don't Do This:**
* Commit secrets directly to your code repository.
**Why:**
* Prevents unauthorized access to sensitive information.
## 4. Testing
### 4.1 Mocking
**Do This:**
* Use mocking libraries (e.g., "sinon", or custom mocks) to simulate API responses during testing.
**Don't Do This:**
* Test against live APIs directly, especially for unit tests.
**Why:**
* Ensures tests are fast, reliable, and isolated. Avoids dependencies on external API availability.
**Example:**
"""typescript
import { assertEquals } from "https://deno.land/std@0.218.2/assert/mod.ts";
import {
APIClient,
ConcreteAPIClient,
User,
} from "./api_abstraction.ts"; // Assumes api_abstraction.ts from earlier examples
// Mock API Client for testing
class MockAPIClient implements APIClient {
private mockUser: User;
constructor(mockUser: User) {
this.mockUser = mockUser;
}
async getUser(id: string): Promise {
return this.mockUser;
}
}
Deno.test("Test getUser with mock API", async () => {
const expectedUser: User = {
id: "123",
name: "Test User",
email: "test@example.com",
};
const mockApiClient: APIClient = new MockAPIClient(expectedUser);
const actualUser = await mockApiClient.getUser("123");
assertEquals(actualUser, expectedUser);
});
"""
## 5. Monitoring and Logging
### 5.1 Logging
**Do This:**
* Log API requests, responses, and errors with sufficient detail for debugging and auditing. Use structured logging for easier analysis.
**Don't Do This:**
* Log sensitive data (e.g., API keys, passwords) directly. Don't omit logging of API interactions.
**Why:**
* Provides visibility into API usage patterns and helps identify potential issues.
### 5.2 Monitoring
**Do This:**
* Monitor API response times, error rates, and usage patterns to detect performance bottlenecks or anomalies.
**Don't Do This:**
* Ignore API performance or error metrics.
**Why:**
* Proactively identify and address API-related problems.
These standards are intended to provide a solid foundation for building robust and secure API integrations in Deno. Consistent application of these principles will lead to more maintainable, performant, and secure applications. Always refer to the latest Deno documentation and best practices for the most up-to-date guidance.
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.
# 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.
# 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.