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
- Map GraphQL types to Hypermodern models
- Convert queries to GET endpoints
- Convert mutations to POST/PUT/DELETE endpoints
- Handle data loading explicitly (no automatic field resolution)
- Add real-time subscriptions using WebSocket streaming
- 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 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.