Skip to main content

Appendix D: Migration Guides

Migrating from REST APIs

From Express.js

If you're coming from Express.js, here's how to migrate your existing REST API to Hypermodern.

Before (Express.js)

const express = require('express');
const app = express();

app.use(express.json());

// User routes
app.get('/users/:id', async (req, res) => {
  try {
    const user = await getUserById(req.params.id);
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/users', async (req, res) => {
  try {
    const { username, email } = req.body;
    const user = await createUser({ username, email });
    res.status(201).json(user);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

app.listen(3000);

After (Hypermodern)

1. Define Schema (schemas/api.json)

{
  "models": {
    "user": {
      "id": "int64",
      "username": "string",
      "email": "string",
      "created_at": "datetime"
    }
  },
  "endpoints": {
    "get_user": {
      "method": "GET",
      "path": "/users/{id}",
      "request": {
        "id": "int64"
      },
      "response": "@user",
      "errors": ["not_found"],
      "transports": ["http", "websocket", "tcp"]
    },
    "create_user": {
      "method": "POST",
      "path": "/users",
      "request": {
        "username": "string",
        "email": "string"
      },
      "response": "@user",
      "errors": ["validation_error"],
      "transports": ["http", "websocket", "tcp"]
    }
  }
}

2. Implement Server (lib/main.dart)

import 'package:hypermodern_server/hypermodern_server.dart';
import 'generated/models.dart';

void main() async {
  final server = HypermodernServer();
  
  // Register endpoints
  server.registerEndpoint<GetUserRequest, User>(
    'get_user',
    (request) async {
      final user = await getUserById(request.id);
      if (user == null) {
        throw NotFoundException('User not found');
      }
      return user;
    },
  );
  
  server.registerEndpoint<CreateUserRequest, User>(
    'create_user',
    (request) async {
      return await createUser(
        username: request.username,
        email: request.email,
      );
    },
  );
  
  await server.listen(httpPort: 3000);
}

3. Generate Code

hypermodern generate

Migration Benefits

  • Multi-protocol support: Your API now works over HTTP, WebSocket, and TCP
  • Type safety: Compile-time type checking for requests and responses
  • Auto-generated clients: Client libraries generated automatically
  • Binary serialization: Better performance than JSON
  • Built-in validation: Request validation handled automatically

From FastAPI (Python)

Before (FastAPI)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class User(BaseModel):
    id: int
    username: str
    email: str

class CreateUserRequest(BaseModel):
    username: str
    email: str

@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
    user = await get_user_by_id(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

@app.post("/users", response_model=User)
async def create_user(request: CreateUserRequest):
    return await create_user_in_db(request.username, request.email)

After (Hypermodern)

The Hypermodern implementation is the same as shown above. The key differences:

  • Schema-first: Define your API contract in JSON schemas
  • Multi-protocol: Automatic support for WebSocket and TCP
  • Code generation: Models and endpoints generated from schemas
  • Better performance: Binary serialization by default

Migration Checklist

  • Define data models in JSON schemas
  • Define endpoints with request/response types
  • Implement endpoint handlers
  • Set up middleware (auth, CORS, rate limiting)
  • Migrate database operations
  • Update client applications
  • Test all protocols (HTTP, WebSocket, TCP)

Migrating from GraphQL

From Apollo Server

Before (Apollo Server)

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type User {
    id: ID!
    username: String!
    email: String!
    posts: [Post!]!
  }
  
  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }
  
  type Query {
    user(id: ID!): User
    users: [User!]!
  }
  
  type Mutation {
    createUser(username: String!, email: String!): User!
  }
`;

const resolvers = {
  Query: {
    user: (_, { id }) => getUserById(id),
    users: () => getAllUsers(),
  },
  Mutation: {
    createUser: (_, { username, email }) => createUser({ username, email }),
  },
  User: {
    posts: (user) => getPostsByUserId(user.id),
  },
  Post: {
    author: (post) => getUserById(post.authorId),
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen();

After (Hypermodern)

1. Define Schema

{
  "models": {
    "user": {
      "id": "int64",
      "username": "string",
      "email": "string",
      "posts": ["@post"]
    },
    "post": {
      "id": "int64",
      "title": "string",
      "content": "string",
      "author": "@user"
    }
  },
  "endpoints": {
    "get_user": {
      "method": "GET",
      "path": "/users/{id}",
      "request": { "id": "int64" },
      "response": "@user",
      "transports": ["http", "websocket", "tcp"]
    },
    "get_users": {
      "method": "GET",
      "path": "/users",
      "request": {},
      "response": { "users": ["@user"] },
      "transports": ["http", "websocket", "tcp"]
    },
    "create_user": {
      "method": "POST",
      "path": "/users",
      "request": {
        "username": "string",
        "email": "string"
      },
      "response": "@user",
      "transports": ["http", "websocket", "tcp"]
    }
  }
}

2. Implement Server

void main() async {
  final server = HypermodernServer();
  
  server.registerEndpoint<GetUserRequest, User>(
    'get_user',
    (request) async {
      final user = await getUserById(request.id);
      if (user == null) {
        throw NotFoundException('User not found');
      }
      
      // Load related posts
      final posts = await getPostsByUserId(user.id);
      return user.copyWith(posts: posts);
    },
  );
  
  server.registerEndpoint<GetUsersRequest, GetUsersResponse>(
    'get_users',
    (request) async {
      final users = await getAllUsers();
      
      // Load posts for all users
      for (final user in users) {
        final posts = await getPostsByUserId(user.id);
        user.posts = posts;
      }
      
      return GetUsersResponse(users: users);
    },
  );
  
  server.registerEndpoint<CreateUserRequest, User>(
    'create_user',
    (request) async {
      return await createUser(
        username: request.username,
        email: request.email,
      );
    },
  );
  
  await server.listen();
}

Key Differences

GraphQL Hypermodern
Single endpoint Multiple typed endpoints
Query language Direct method calls
Schema-first Schema-first
HTTP only HTTP + WebSocket + TCP
JSON only Binary + JSON
Resolver functions Endpoint handlers
Field-level fetching Explicit data loading

Migration Strategy

  1. Map GraphQL types to Hypermodern models
  2. Convert queries to GET endpoints
  3. Convert mutations to POST/PUT/DELETE endpoints
  4. Handle data loading explicitly (no automatic field resolution)
  5. Add real-time subscriptions using WebSocket streaming
  6. Optimize with binary serialization

Migrating from gRPC

From gRPC Service

Before (gRPC Proto)

syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc CreateUser(CreateUserRequest) returns (User);
  rpc StreamUsers(StreamUsersRequest) returns (stream User);
}

message User {
  int64 id = 1;
  string username = 2;
  string email = 3;
}

message GetUserRequest {
  int64 id = 1;
}

message CreateUserRequest {
  string username = 1;
  string email = 2;
}

message StreamUsersRequest {
  repeated int64 user_ids = 1;
}

After (Hypermodern)

1. Schema Definition

{
  "models": {
    "user": {
      "id": "int64",
      "username": "string",
      "email": "string"
    }
  },
  "endpoints": {
    "get_user": {
      "method": "GET",
      "path": "/users/{id}",
      "request": { "id": "int64" },
      "response": "@user",
      "transports": ["http", "websocket", "tcp"]
    },
    "create_user": {
      "method": "POST",
      "path": "/users",
      "request": {
        "username": "string",
        "email": "string"
      },
      "response": "@user",
      "transports": ["http", "websocket", "tcp"]
    },
    "stream_users": {
      "type": "stream",
      "request": {
        "user_ids": ["int64"]
      },
      "response": "@user",
      "transports": ["websocket", "tcp"]
    }
  }
}

2. Server Implementation

void main() async {
  final server = HypermodernServer();
  
  // Regular endpoints
  server.registerEndpoint<GetUserRequest, User>(
    'get_user',
    (request) async => await getUserById(request.id),
  );
  
  server.registerEndpoint<CreateUserRequest, User>(
    'create_user',
    (request) async => await createUser(request.username, request.email),
  );
  
  // Streaming endpoint
  server.registerStreamingEndpoint<StreamUsersRequest, User>(
    'stream_users',
    (request) async* {
      for (final userId in request.userIds) {
        final user = await getUserById(userId);
        if (user != null) {
          yield user;
        }
      }
    },
  );
  
  await server.listen();
}

Migration Benefits

  • Protocol flexibility: Support HTTP, WebSocket, and TCP (not just HTTP/2)
  • Easier debugging: Human-readable JSON fallback
  • Web compatibility: Works in browsers without proxies
  • Simpler tooling: Standard HTTP tools work
  • Multi-language: Not limited to gRPC-supported languages

Migrating to UUID v7 and Vector Data Types

Enabling UUID v7 Support

Prerequisites

  1. PostgreSQL Extensions: Ensure your PostgreSQL instance supports the required extensions:
-- Check available extensions
SELECT name, installed_version, comment 
FROM pg_available_extensions 
WHERE name IN ('uuid-ossp', 'pgcrypto');
  1. Migration Setup: Create the enabling migration:
// lib/database/migrations/001_enable_uuid_extensions.dart
import 'package:hypermodern_server/database.dart';

class EnableUuidExtensions extends Migration {
  @override
  Future<void> up() async {
    await execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";');
    await execute('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
    
    // Create UUID v7 generation function
    await execute('''
      CREATE OR REPLACE FUNCTION generate_uuid_v7()
      RETURNS UUID AS \$\$
      DECLARE
          unix_ts_ms BIGINT;
          uuid_bytes BYTEA;
      BEGIN
          unix_ts_ms := FLOOR(EXTRACT(EPOCH FROM NOW()) * 1000);
          uuid_bytes := 
              SUBSTRING(INT8SEND(unix_ts_ms), 3, 6) ||
              GEN_RANDOM_BYTES(2) ||
              (B'0111' || SUBSTRING(GEN_RANDOM_BYTES(1), 1, 1)::BIT(4))::BIT(8)::BYTEA ||
              GEN_RANDOM_BYTES(7);
          RETURN ENCODE(uuid_bytes, 'hex')::UUID;
      END;
      \$\$ LANGUAGE plpgsql;
    ''');
  }

  @override
  Future<void> down() async {
    await execute('DROP FUNCTION IF EXISTS generate_uuid_v7();');
  }
}

Migrating Existing Tables to UUID v7

Step 1: Add UUID v7 Column

class AddUuidV7ToUsers extends Migration {
  @override
  Future<void> up() async {
    // Add new UUID v7 column
    await execute('''
      ALTER TABLE users 
      ADD COLUMN uuid_id UUID DEFAULT generate_uuid_v7() NOT NULL;
    ''');
    
    // Create unique index
    await execute('CREATE UNIQUE INDEX idx_users_uuid_id ON users (uuid_id);');
  }

  @override
  Future<void> down() async {
    await execute('ALTER TABLE users DROP COLUMN uuid_id;');
  }
}

Step 2: Update Application Code

// Before: Using integer IDs
class User {
  final int id;
  final String username;
  
  User({required this.id, required this.username});
}

// After: Using UUID v7
class User {
  final String id;        // UUID v7 string
  final int? legacyId;    // Keep for migration period
  final String username;
  
  User({required this.id, this.legacyId, required this.username});
}

Step 3: Gradual Migration

class UserService {
  // Support both ID types during migration
  Future<User?> getUser({int? legacyId, String? uuidId}) async {
    if (uuidId != null) {
      return await db.query('SELECT * FROM users WHERE uuid_id = ?', [uuidId]);
    } else if (legacyId != null) {
      return await db.query('SELECT * FROM users WHERE id = ?', [legacyId]);
    }
    throw ArgumentError('Either legacyId or uuidId must be provided');
  }
}

Enabling Vector Support (pgvector)

Prerequisites

  1. Install pgvector Extension:
# Ubuntu/Debian
sudo apt install postgresql-14-pgvector

# macOS with Homebrew
brew install pgvector

# Or compile from source
git clone https://github.com/pgvector/pgvector.git
cd pgvector
make
sudo make install
  1. Enable Extension Migration:
class EnablePgVectorExtension extends Migration {
  @override
  Future<void> up() async {
    await execute('CREATE EXTENSION IF NOT EXISTS vector;');
  }

  @override
  Future<void> down() async {
    await execute('DROP EXTENSION IF EXISTS vector CASCADE;');
  }
}

Adding Vector Columns to Existing Tables

class AddEmbeddingsToDocuments extends Migration {
  @override
  Future<void> up() async {
    // Add vector column for embeddings
    await execute('''
      ALTER TABLE documents 
      ADD COLUMN embedding VECTOR(1536);
    ''');
    
    // Create HNSW index for similarity search
    await execute('''
      CREATE INDEX idx_documents_embedding_hnsw 
      ON documents USING hnsw (embedding vector_cosine_ops) 
      WITH (m = 16, ef_construction = 64);
    ''');
  }

  @override
  Future<void> down() async {
    await execute('ALTER TABLE documents DROP COLUMN embedding;');
  }
}

Populating Vector Data

class PopulateDocumentEmbeddings extends Migration {
  @override
  Future<void> up() async {
    final documents = await db.query('SELECT id, content FROM documents WHERE embedding IS NULL');
    
    for (final doc in documents) {
      // Generate embedding (example using OpenAI)
      final embedding = await generateEmbedding(doc['content']);
      final vectorString = '[${embedding.join(',')}]';
      
      await execute('''
        UPDATE documents 
        SET embedding = ? 
        WHERE id = ?
      ''', [vectorString, doc['id']]);
    }
  }

  @override
  Future<void> down() async {
    await execute('UPDATE documents SET embedding = NULL;');
  }
}

Complete Migration Example

Here's a complete example migrating a blog system to use UUID v7 and vector embeddings:

// Migration 1: Enable extensions
class EnableAdvancedExtensions extends Migration {
  @override
  Future<void> up() async {
    await execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";');
    await execute('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
    await execute('CREATE EXTENSION IF NOT EXISTS vector;');
    
    await execute(UuidHelper.postgresGenerateV7Function);
  }

  @override
  Future<void> down() async {
    await execute('DROP FUNCTION IF EXISTS generate_uuid_v7();');
    await execute('DROP EXTENSION IF EXISTS vector CASCADE;');
  }
}

// Migration 2: Create new table with modern types
class CreateModernBlogPosts extends Migration {
  @override
  Future<void> up() async {
    create('blog_posts_v2', (Schema table) {
      // UUID v7 primary key
      table.addColumn('id', 'UUID', 
        nullable: false, 
        defaultValue: 'generate_uuid_v7()'
      );
      table.primary('id');
      
      // Content fields
      table.addColumn('title', 'VARCHAR', length: 255, nullable: false);
      table.addColumn('content', 'TEXT', nullable: false);
      table.addColumn('author_id', 'UUID', nullable: false);
      
      // Vector embedding for content similarity
      table.addColumn('content_embedding', 'VECTOR(1536)', nullable: true);
      
      // Timestamps
      table.addColumn('created_at', 'TIMESTAMP WITH TIME ZONE', 
        defaultValue: 'NOW()', nullable: false);
      table.addColumn('updated_at', 'TIMESTAMP WITH TIME ZONE', 
        defaultValue: 'NOW()', nullable: false);
    });
    
    // Create vector similarity index
    await execute('''
      CREATE INDEX idx_blog_posts_v2_content_embedding_hnsw 
      ON blog_posts_v2 USING hnsw (content_embedding vector_cosine_ops) 
      WITH (m = 16, ef_construction = 64);
    ''');
  }

  @override
  Future<void> down() async {
    await drop('blog_posts_v2');
  }
}

// Migration 3: Data migration with embedding generation
class MigrateBlogPostsData extends Migration {
  @override
  Future<void> up() async {
    final oldPosts = await db.query('''
      SELECT id, title, content, author_id, created_at, updated_at 
      FROM blog_posts
    ''');
    
    for (final post in oldPosts) {
      // Generate UUID v7 for new ID
      final newId = UuidV7.generate();
      
      // Generate content embedding
      final embedding = await generateContentEmbedding(post['content']);
      final embeddingVector = '[${embedding.join(',')}]';
      
      await execute('''
        INSERT INTO blog_posts_v2 (id, title, content, author_id, content_embedding, created_at, updated_at)
        VALUES (?, ?, ?, ?, ?, ?, ?)
      ''', [
        newId,
        post['title'],
        post['content'],
        post['author_id'], // Assume author IDs are already UUIDs
        embeddingVector,
        post['created_at'],
        post['updated_at'],
      ]);
    }
  }

  @override
  Future<void> down() async {
    await execute('DELETE FROM blog_posts_v2;');
  }
}

Vector Similarity Search Implementation

After migration, implement similarity search:

class BlogPostService {
  Future<List<BlogPost>> findSimilarPosts(String content, {
    int limit = 10,
    double threshold = 0.7,
  }) async {
    // Generate embedding for query content
    final queryEmbedding = await generateContentEmbedding(content);
    final queryVector = '[${queryEmbedding.join(',')}]';
    
    final results = await db.query('''
      SELECT 
        id, title, content, author_id, created_at,
        1 - (content_embedding <=> ?) as similarity
      FROM blog_posts_v2
      WHERE content_embedding IS NOT NULL
        AND 1 - (content_embedding <=> ?) > ?
      ORDER BY content_embedding <=> ?
      LIMIT ?
    ''', [queryVector, queryVector, threshold, queryVector, limit]);
    
    return results.map((row) => BlogPost.fromMap(row)).toList();
  }
}

Performance Considerations

  1. UUID v7 Indexing: UUID v7 performs better in B-tree indexes due to time-ordering
  2. Vector Index Tuning: Adjust HNSW parameters based on your data size:
    • m: 16 for most cases, 32 for high recall
    • ef_construction: 64-200 depending on build time vs. accuracy trade-off
  3. Batch Operations: Use batch inserts for large vector datasets
  4. Memory Usage: Vector operations can be memory-intensive; monitor usage

Migrating Databases

From MongoDB to PostgreSQL

Data Migration Script

class MongoToPostgresMigration {
  final MongoDatabase _mongo;
  final Database _postgres;
  
  MongoToPostgresMigration(this._mongo, this._postgres);
  
  Future<void> migrateUsers() async {
    print('Migrating users...');
    
    final users = _mongo.collection('users');
    final cursor = users.find();
    
    await for (final doc in cursor) {
      final user = _transformUser(doc);
      
      await _postgres.query('''
        INSERT INTO users (id, username, email, created_at, metadata)
        VALUES (?, ?, ?, ?, ?)
      ''', [
        user['id'],
        user['username'],
        user['email'],
        user['created_at'],
        jsonEncode(user['metadata']),
      ]);
    }
    
    print('Users migration completed');
  }
  
  Map<String, dynamic> _transformUser(Map<String, dynamic> mongoDoc) {
    return {
      'id': mongoDoc['_id'].toString(),
      'username': mongoDoc['username'],
      'email': mongoDoc['email'],
      'created_at': mongoDoc['createdAt'] ?? DateTime.now(),
      'metadata': mongoDoc['profile'] ?? {},
    };
  }
}

From MySQL to PostgreSQL

Schema Migration

-- MySQL to PostgreSQL type mappings
-- TINYINT(1) -> BOOLEAN
-- VARCHAR(255) -> VARCHAR(255)
-- TEXT -> TEXT
-- DATETIME -> TIMESTAMP WITH TIME ZONE
-- JSON -> JSONB

-- Before (MySQL)
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  is_active TINYINT(1) DEFAULT 1,
  profile JSON,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- After (PostgreSQL)
CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL UNIQUE,
  is_active BOOLEAN DEFAULT true,
  profile JSONB,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Data Migration

class MySQLToPostgresMigration {
  final MySQLConnection _mysql;
  final Database _postgres;
  
  Future<void> migrate() async {
    await _migrateUsers();
    await _migratePosts();
    await _updateSequences();
  }
  
  Future<void> _migrateUsers() async {
    final results = await _mysql.query('SELECT * FROM users');
    
    for (final row in results) {
      await _postgres.query('''
        INSERT INTO users (id, username, email, is_active, profile, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
      ''', [
        row['id'],
        row['username'],
        row['email'],
        row['is_active'] == 1,
        row['profile'],
        row['created_at'],
      ]);
    }
  }
  
  Future<void> _updateSequences() async {
    // Update PostgreSQL sequences to match migrated data
    await _postgres.query('''
      SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))
    ''');
  }
}

Client Migration

From Axios to Hypermodern Client

Before (Axios)

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  headers: {
    'Authorization': `Bearer ${token}`
  }
});

// Get user
const getUser = async (id) => {
  try {
    const response = await api.get(`/users/${id}`);
    return response.data;
  } catch (error) {
    throw new Error(error.response.data.message);
  }
};

// Create user
const createUser = async (userData) => {
  try {
    const response = await api.post('/users', userData);
    return response.data;
  } catch (error) {
    throw new Error(error.response.data.message);
  }
};

After (Hypermodern Client)

import 'package:hypermodern/hypermodern.dart';
import 'generated/models.dart';

class ApiClient {
  final HypermodernClient _client;
  
  ApiClient(String baseUrl) : _client = HypermodernClient(baseUrl);
  
  Future<void> connect(String token) async {
    _client.setAuthToken(token);
    await _client.connect();
  }
  
  Future<User> getUser(int id) async {
    return await _client.request<User>(
      'get_user',
      GetUserRequest(id: id),
    );
  }
  
  Future<User> createUser({
    required String username,
    required String email,
  }) async {
    return await _client.request<User>(
      'create_user',
      CreateUserRequest(username: username, email: email),
    );
  }
  
  // Real-time updates (not possible with Axios)
  Stream<User> watchUser(int id) {
    return _client.stream<User>(
      'watch_user',
      WatchUserRequest(id: id),
    );
  }
}

Migration Benefits

  • Type safety: Compile-time type checking
  • Multi-protocol: Can switch between HTTP, WebSocket, TCP
  • Real-time: Built-in streaming support
  • Better performance: Binary serialization
  • Auto-generated: Client code generated from schemas

Testing Migration

From Jest to Dart Test

Before (Jest)

const request = require('supertest');
const app = require('../app');

describe('User API', () => {
  test('should get user by id', async () => {
    const response = await request(app)
      .get('/users/1')
      .expect(200);
    
    expect(response.body).toHaveProperty('id', 1);
    expect(response.body).toHaveProperty('username');
  });
  
  test('should create user', async () => {
    const userData = {
      username: 'testuser',
      email: 'test@example.com'
    };
    
    const response = await request(app)
      .post('/users')
      .send(userData)
      .expect(201);
    
    expect(response.body).toHaveProperty('id');
    expect(response.body.username).toBe(userData.username);
  });
});

After (Dart Test)

import 'package:test/test.dart';
import 'package:hypermodern_server/hypermodern_server.dart';
import 'package:hypermodern/hypermodern.dart';
import 'generated/models.dart';

void main() {
  group('User API', () {
    late HypermodernServer server;
    late HypermodernClient client;
    
    setUpAll(() async {
      server = HypermodernServer();
      // Register endpoints...
      await server.listen();
      
      client = HypermodernClient('http://localhost:8080');
      await client.connect();
    });
    
    tearDownAll(() async {
      await client.disconnect();
      await server.stop();
    });
    
    test('should get user by id', () async {
      final user = await client.request<User>(
        'get_user',
        GetUserRequest(id: 1),
      );
      
      expect(user.id, equals(1));
      expect(user.username, isNotEmpty);
    });
    
    test('should create user', () async {
      final request = CreateUserRequest(
        username: 'testuser',
        email: 'test@example.com',
      );
      
      final user = await client.request<User>('create_user', request);
      
      expect(user.id, isPositive);
      expect(user.username, equals('testuser'));
    });
    
    // Test multiple protocols
    test('should work over WebSocket', () async {
      final wsClient = HypermodernClient('ws://localhost:8082');
      await wsClient.connect();
      
      final user = await wsClient.request<User>(
        'get_user',
        GetUserRequest(id: 1),
      );
      
      expect(user.id, equals(1));
      
      await wsClient.disconnect();
    });
  });
}

Deployment Migration

From Docker Compose to Kubernetes

Before (Docker Compose)

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    depends_on:
      - db
  
  db:
    image: postgres:13
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

After (Kubernetes)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hypermodern-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hypermodern-app
  template:
    metadata:
      labels:
        app: hypermodern-app
    spec:
      containers:
      - name: hypermodern
        image: hypermodern-app:latest
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 8082
          name: websocket
        - containerPort: 8081
          name: tcp
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
---
apiVersion: v1
kind: Service
metadata:
  name: hypermodern-service
spec:
  selector:
    app: hypermodern-app
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: websocket
    port: 8082
    targetPort: 8082
  - name: tcp
    port: 8081
    targetPort: 8081

This migration guide provides step-by-step instructions for migrating from various technologies to Hypermodern, highlighting the benefits and providing practical examples for each migration path.