Files
DynamoDNS/docs/development-guide.md
alphaeusmote ae19ec9c3d Document project architecture, setup, usage, and API endpoints
Adds comprehensive documentation including architecture, API reference, deployment, development, and user guides with README.md.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/910f6160-6716-488e-83e6-61256595c269.jpg
2025-05-24 02:16:17 +00:00

13 KiB

DynamoDNS Development Guide

This guide provides information for developers who want to contribute to the DynamoDNS project or extend its functionality.

Development Setup

Prerequisites

  • Node.js: v18.0.0 or higher
  • npm: v8.0.0 or higher
  • PostgreSQL: v14.0 or higher (or Neon serverless database)
  • Git: Latest version

Getting Started

  1. Clone the repository:

    git clone https://github.com/yourusername/dynamodns.git
    cd dynamodns
    
  2. Install dependencies:

    npm install
    
  3. Set up environment variables: Create a .env file based on the .env.example template:

    # Database - Use a local PostgreSQL instance for development
    DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dynamodns_dev
    
    # Server
    PORT=5000
    NODE_ENV=development
    SESSION_SECRET=dev_session_secret
    
    # Auth - Enable registration for easier testing
    ENABLE_REGISTRATION=true
    
  4. Set up the database:

    # Create the database
    createdb dynamodns_dev
    
    # Run migrations
    npm run db:push
    
  5. Start the development server:

    npm run dev
    
  6. Access the application: Open your browser and navigate to http://localhost:5000

Project Structure

dynamodns/
├── client/                  # Frontend React application
│   ├── src/                 # Source code
│   │   ├── components/      # Reusable UI components
│   │   ├── context/         # React context providers
│   │   ├── hooks/           # Custom React hooks
│   │   ├── lib/             # Utility functions
│   │   ├── pages/           # Page components
│   │   └── App.tsx          # Main application component
├── docs/                    # Documentation
├── migrations/              # Database migration files
├── server/                  # Backend Express application
│   ├── providers/           # DNS provider integrations
│   ├── utils/               # Utility functions
│   ├── auth.ts              # Authentication logic
│   ├── db.ts                # Database connection
│   ├── index.ts             # Application entry point
│   ├── routes.ts            # API routes
│   ├── storage.ts           # Storage interface
│   └── vite.ts              # Vite integration
├── shared/                  # Shared code between client and server
│   └── schema.ts            # Database schema with Drizzle ORM
└── various config files     # Configuration files for the project

Technology Stack

Backend

  • Express.js: Web server framework
  • Drizzle ORM: Database ORM for TypeScript
  • PostgreSQL: Relational database
  • Passport.js: Authentication middleware
  • WebSockets: Real-time communication

Frontend

  • React: UI library
  • TanStack Query: Data fetching and caching
  • Shadcn UI: Component library built on Radix UI
  • Tailwind CSS: Utility-first CSS framework
  • Wouter: Lightweight routing
  • Zod: Schema validation
  • React Hook Form: Form handling

Architecture

Frontend Architecture

The frontend follows a component-based architecture with React:

  • Context Providers: Manage global state (auth, customer, theme)
  • Hooks: Custom hooks for data fetching and business logic
  • Pages: Top-level components for different routes
  • Components: Reusable UI building blocks

Data flow utilizes TanStack Query for client-side data fetching and caching:

// Example query
const { data, isLoading, error } = useQuery({
  queryKey: ['/api/domains'],
  enabled: !!user
});

// Example mutation
const mutation = useMutation({
  mutationFn: async (data) => {
    const res = await apiRequest('POST', '/api/domains', data);
    return await res.json();
  },
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['/api/domains'] });
    // Handle success
  },
  onError: (error) => {
    // Handle error
  }
});

Backend Architecture

The backend follows a layered architecture:

  1. Routes Layer: API endpoints and request handling
  2. Service Layer: Business logic
  3. Data Access Layer: Database interactions via Drizzle ORM
  4. Provider Layer: Integrations with external DNS services

The application uses a storage interface pattern to abstract database operations:

// Example storage interface method
async getUser(id: string): Promise<User | undefined> {
  const [user] = await db.select().from(users).where(eq(users.id, id));
  return user || undefined;
}

Adding Features

Adding a New DNS Provider

  1. Create a provider module: Create a new file in server/providers/ directory (e.g., cloudflare.ts).

  2. Implement the provider interface:

    import { DNSProvider, DNSRecord, DNSProviderError } from './types';
    
    export class CloudflareProvider implements DNSProvider {
      constructor(private credentials: any) {
        // Initialize with provider credentials
      }
    
      async createRecord(record: DNSRecord): Promise<string> {
        // Implementation
      }
    
      async updateRecord(recordId: string, record: DNSRecord): Promise<void> {
        // Implementation
      }
    
      async deleteRecord(recordId: string): Promise<void> {
        // Implementation
      }
    
      async getRecord(recordId: string): Promise<DNSRecord> {
        // Implementation
      }
    }
    
  3. Register the provider: Add the provider to the provider factory in server/providers/index.ts.

Adding a New API Endpoint

  1. Create the route handler: Add your endpoint to server/routes.ts:

    app.get('/api/your-endpoint', (req, res) => {
      // Implementation
    });
    
  2. Add validation: Use Zod to validate request inputs:

    const schema = z.object({
      name: z.string().min(1),
      // Other fields
    });
    
    app.post('/api/your-endpoint', (req, res) => {
      const result = schema.safeParse(req.body);
      if (!result.success) {
        return res.status(400).json({ errors: result.error.format() });
      }
    
      // Implementation with validated data
      const data = result.data;
    });
    
  3. Use the storage interface: Interact with the database using the storage interface:

    app.get('/api/your-endpoint/:id', async (req, res) => {
      try {
        const item = await storage.getItem(req.params.id);
        if (!item) {
          return res.status(404).json({ message: 'Not found' });
        }
        res.json(item);
      } catch (error) {
        res.status(500).json({ message: 'Server error' });
      }
    });
    

Adding a New UI Component

  1. Create the component: Create a new file in the appropriate directory under client/src/components/.

  2. Implement the component:

    import React from 'react';
    import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
    
    interface MyComponentProps {
      title: string;
      children: React.ReactNode;
    }
    
    export function MyComponent({ title, children }: MyComponentProps) {
      return (
        <Card>
          <CardHeader>
            <CardTitle>{title}</CardTitle>
          </CardHeader>
          <CardContent>
            {children}
          </CardContent>
        </Card>
      );
    }
    
  3. Use the component: Import and use your component in a page or another component.

Database Schema Management

The project uses Drizzle ORM with a schema defined in shared/schema.ts.

Adding a New Table

  1. Define the table:

    export const myNewTable = pgTable('my_new_table', {
      id: text('id').primaryKey().defaultRandom(),
      name: text('name').notNull(),
      createdAt: timestamp('created_at').defaultNow().notNull(),
      customerId: text('customer_id').references(() => customers.id, { onDelete: 'cascade' }),
    });
    
  2. Define relations:

    export const myNewTableRelations = relations(myNewTable, ({ one }) => ({
      customer: one(customers, {
        fields: [myNewTable.customerId],
        references: [customers.id],
      }),
    }));
    
  3. Create insert schema and types:

    export const insertMyNewTableSchema = createInsertSchema(myNewTable).pick({
      name: true,
      customerId: true,
    });
    
    export type InsertMyNewTable = z.infer<typeof insertMyNewTableSchema>;
    export type MyNewTable = typeof myNewTable.$inferSelect;
    
  4. Update the storage interface: Add methods to server/storage.ts for CRUD operations on your new table.

  5. Push the schema changes:

    npm run db:push
    

Testing

Unit Tests

The project uses Jest for unit testing:

# Run all tests
npm test

# Run tests with coverage
npm test -- --coverage

# Run specific test file
npm test -- src/path/to/test.test.ts

Writing Tests

Example test for a utility function:

// src/utils/formatDate.test.ts
import { formatDate } from './formatDate';

describe('formatDate', () => {
  it('formats a date correctly', () => {
    const date = new Date('2023-01-01T12:00:00Z');
    expect(formatDate(date)).toBe('Jan 1, 2023');
  });

  it('handles null values', () => {
    expect(formatDate(null)).toBe('');
  });
});

Example test for a React component using React Testing Library:

// src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders correctly', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

API Testing

For API endpoints, use Supertest:

// src/routes/domains.test.ts
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';

describe('Domains API', () => {
  beforeEach(async () => {
    // Set up test data
  });

  afterEach(async () => {
    // Clean up test data
  });

  it('GET /api/domains returns domains', async () => {
    const response = await request(app)
      .get('/api/domains')
      .set('Authorization', `Bearer ${testToken}`);
    
    expect(response.status).toBe(200);
    expect(response.body).toHaveLength(2);
  });
});

Code Style and Linting

The project uses ESLint and Prettier for code style and linting:

# Run ESLint
npm run lint

# Fix ESLint issues
npm run lint:fix

# Format code with Prettier
npm run format

Commit Guidelines

We follow the Conventional Commits specification:

  • feat: A new feature
  • fix: A bug fix
  • docs: Documentation changes
  • style: Changes that do not affect the meaning of the code
  • refactor: Code changes that neither fix a bug nor add a feature
  • perf: Performance improvements
  • test: Adding or fixing tests
  • chore: Changes to the build process or auxiliary tools

Example:

feat(dns-records): add support for SRV records

Debugging

Server-side Debugging

Add logging with debug statements:

import debug from 'debug';

const log = debug('dynamodns:server');

app.get('/api/domains', (req, res) => {
  log('Fetching domains for user', req.user?.id);
  // Implementation
});

Run the application with debug enabled:

DEBUG=dynamodns:* npm run dev

Client-side Debugging

Use React DevTools for component inspection and debugging.

For network debugging, use the browser's developer tools and the Network tab.

Deployment

See the Deployment Guide for information on deploying the application.

Contributing

  1. Fork the repository
  2. Create a feature branch:
    git checkout -b feature/your-feature-name
    
  3. Make your changes
  4. Run tests:
    npm test
    
  5. Commit your changes following the commit guidelines
  6. Push to your fork:
    git push origin feature/your-feature-name
    
  7. Create a Pull Request

Resources