Widget Versioning Best Practices: Semantic Versioning Guide
Widget versioning is the cornerstone of reliable ChatGPT app development. Without proper version management, updates can break existing integrations, frustrate users, and create support nightmares. In ChatGPT's conversational environment, where widgets appear in chat histories that may be revisited days or weeks later, versioning becomes critical for maintaining consistent user experiences.
This guide provides production-ready strategies for implementing semantic versioning, maintaining backward compatibility, managing deprecations gracefully, and creating smooth migration paths for your ChatGPT widgets. Whether you're building your first widget or managing a suite of production widgets serving thousands of users, these patterns will help you ship updates confidently without breaking existing functionality.
Version management isn't just about numbering releases—it's about establishing trust with your users. When developers integrate your widget into their ChatGPT apps, they need assurance that future updates won't unexpectedly break their implementations. By following semantic versioning principles and implementing robust compatibility layers, you signal professionalism and reliability.
Understanding Semantic Versioning for Widgets
Semantic Versioning (SemVer) provides a universal language for communicating the nature of changes in your widget. The format MAJOR.MINOR.PATCH (e.g., 2.4.1) conveys specific guarantees about compatibility and functionality changes.
MAJOR version increments (1.x.x → 2.0.0) signal breaking changes that require consumers to update their integration code. This might include removing deprecated APIs, changing function signatures, or restructuring data formats. Major versions are your opportunity to clean up technical debt and make significant architectural improvements.
MINOR version increments (2.3.x → 2.4.0) introduce new features while maintaining backward compatibility. Existing integrations continue working, but new capabilities become available. This is the sweet spot for most updates—adding value without disruption.
PATCH version increments (2.4.1 → 2.4.2) address bugs and security issues without changing functionality. These should be safe to deploy immediately and require no consumer action.
Here's a production-ready version manager that enforces SemVer rules:
// version-manager.ts
interface Version {
major: number;
minor: number;
patch: number;
prerelease?: string;
build?: string;
}
interface ChangeType {
type: 'breaking' | 'feature' | 'fix';
description: string;
affectedAPIs?: string[];
}
class VersionManager {
private currentVersion: Version;
private changelog: Map<string, ChangeType[]> = new Map();
constructor(initialVersion: string) {
this.currentVersion = this.parseVersion(initialVersion);
}
parseVersion(versionString: string): Version {
// Parse SemVer format: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
const regex = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
const match = versionString.match(regex);
if (!match) {
throw new Error(`Invalid version format: ${versionString}`);
}
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
prerelease: match[4],
build: match[5]
};
}
toString(version: Version): string {
let versionString = `${version.major}.${version.minor}.${version.patch}`;
if (version.prerelease) versionString += `-${version.prerelease}`;
if (version.build) versionString += `+${version.build}`;
return versionString;
}
incrementMajor(changes: ChangeType[]): Version {
const newVersion = {
major: this.currentVersion.major + 1,
minor: 0,
patch: 0
};
this.recordChanges(this.toString(newVersion), changes);
this.currentVersion = newVersion;
return newVersion;
}
incrementMinor(changes: ChangeType[]): Version {
const newVersion = {
...this.currentVersion,
minor: this.currentVersion.minor + 1,
patch: 0
};
this.recordChanges(this.toString(newVersion), changes);
this.currentVersion = newVersion;
return newVersion;
}
incrementPatch(changes: ChangeType[]): Version {
const newVersion = {
...this.currentVersion,
patch: this.currentVersion.patch + 1
};
this.recordChanges(this.toString(newVersion), changes);
this.currentVersion = newVersion;
return newVersion;
}
private recordChanges(version: string, changes: ChangeType[]) {
this.changelog.set(version, changes);
}
compare(v1: Version, v2: Version): number {
// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
if (v1.major !== v2.major) return v1.major > v2.major ? 1 : -1;
if (v1.minor !== v2.minor) return v1.minor > v2.minor ? 1 : -1;
if (v1.patch !== v2.patch) return v1.patch > v2.patch ? 1 : -1;
return 0;
}
isCompatible(clientVersion: string, serverVersion: string): boolean {
const client = this.parseVersion(clientVersion);
const server = this.parseVersion(serverVersion);
// Major versions must match for compatibility
if (client.major !== server.major) return false;
// Server minor/patch can be higher (backward compatible)
return this.compare(server, client) >= 0;
}
getChangelog(version: string): ChangeType[] | undefined {
return this.changelog.get(version);
}
generateReleaseNotes(version: string): string {
const changes = this.getChangelog(version);
if (!changes) return '';
const breaking = changes.filter(c => c.type === 'breaking');
const features = changes.filter(c => c.type === 'feature');
const fixes = changes.filter(c => c.type === 'fix');
let notes = `# Release ${version}\n\n`;
if (breaking.length > 0) {
notes += `## ⚠️ BREAKING CHANGES\n\n`;
breaking.forEach(change => {
notes += `- ${change.description}\n`;
if (change.affectedAPIs?.length) {
notes += ` - Affected APIs: ${change.affectedAPIs.join(', ')}\n`;
}
});
notes += '\n';
}
if (features.length > 0) {
notes += `## ✨ New Features\n\n`;
features.forEach(change => {
notes += `- ${change.description}\n`;
});
notes += '\n';
}
if (fixes.length > 0) {
notes += `## 🐛 Bug Fixes\n\n`;
fixes.forEach(change => {
notes += `- ${change.description}\n`;
});
}
return notes;
}
}
// Usage example
const versionManager = new VersionManager('1.4.2');
// Bug fix release
versionManager.incrementPatch([
{
type: 'fix',
description: 'Fixed memory leak in state persistence'
}
]);
// Feature release
versionManager.incrementMinor([
{
type: 'feature',
description: 'Added dark mode support with theme detection'
},
{
type: 'fix',
description: 'Improved accessibility for screen readers'
}
]);
// Breaking change release
const newMajor = versionManager.incrementMajor([
{
type: 'breaking',
description: 'Removed deprecated setState() method',
affectedAPIs: ['setState', 'getState']
},
{
type: 'breaking',
description: 'Changed event handler signatures to async',
affectedAPIs: ['onClick', 'onSubmit']
}
]);
console.log(versionManager.generateReleaseNotes('2.0.0'));
This version manager enforces SemVer rules, tracks changes, and generates human-readable release notes automatically. The isCompatible() method is particularly valuable for runtime version checking.
Maintaining Backward Compatibility
Backward compatibility is the art of adding new capabilities without breaking existing integrations. For ChatGPT widgets, this is especially important because chat histories may reference older widget versions, and users expect consistent behavior when revisiting conversations.
The adapter pattern is your best friend for maintaining compatibility across versions. Adapters translate between old and new APIs, allowing you to modernize internals while preserving legacy interfaces.
// compatibility-adapter.ts
interface WidgetStateV1 {
data: any;
timestamp: number;
}
interface WidgetStateV2 {
data: any;
metadata: {
timestamp: number;
version: string;
userId?: string;
};
}
interface WidgetStateV3 {
payload: any; // Renamed from 'data'
metadata: {
createdAt: number; // Renamed from 'timestamp'
updatedAt: number;
version: string;
userId?: string;
sessionId?: string;
};
}
class CompatibilityAdapter {
private readonly currentVersion = '3.0.0';
private adapters: Map<string, (state: any) => any> = new Map();
constructor() {
this.registerAdapters();
}
private registerAdapters() {
// V1 → V2 adapter
this.adapters.set('1.x->2.x', (state: WidgetStateV1): WidgetStateV2 => {
return {
data: state.data,
metadata: {
timestamp: state.timestamp,
version: '2.0.0'
}
};
});
// V2 → V3 adapter
this.adapters.set('2.x->3.x', (state: WidgetStateV2): WidgetStateV3 => {
return {
payload: state.data,
metadata: {
createdAt: state.metadata.timestamp,
updatedAt: Date.now(),
version: '3.0.0',
userId: state.metadata.userId
}
};
});
// Direct V1 → V3 adapter (optimization)
this.adapters.set('1.x->3.x', (state: WidgetStateV1): WidgetStateV3 => {
return {
payload: state.data,
metadata: {
createdAt: state.timestamp,
updatedAt: Date.now(),
version: '3.0.0'
}
};
});
}
adaptState(state: any, fromVersion: string): any {
const from = this.getMajorVersion(fromVersion);
const to = this.getMajorVersion(this.currentVersion);
if (from === to) return state;
// Try direct adapter first
const directKey = `${from}.x->${to}.x`;
if (this.adapters.has(directKey)) {
return this.adapters.get(directKey)!(state);
}
// Fall back to chain of adapters
return this.chainAdapters(state, from, to);
}
private chainAdapters(state: any, from: number, to: number): any {
let currentState = state;
let currentVersion = from;
while (currentVersion < to) {
const nextVersion = currentVersion + 1;
const key = `${currentVersion}.x->${nextVersion}.x`;
if (!this.adapters.has(key)) {
throw new Error(`No adapter found for ${key}`);
}
currentState = this.adapters.get(key)!(currentState);
currentVersion = nextVersion;
}
return currentState;
}
private getMajorVersion(version: string): number {
return parseInt(version.split('.')[0], 10);
}
// Backward compatibility wrapper for deprecated methods
createLegacyAPI(modernAPI: any) {
return {
// V1 API (deprecated in V2)
setState: (data: any) => {
console.warn('setState() is deprecated. Use updateState() instead.');
return modernAPI.updateState({ payload: data });
},
getState: () => {
console.warn('getState() is deprecated. Use getCurrentState() instead.');
const state = modernAPI.getCurrentState();
// Convert V3 format back to V1 format
return {
data: state.payload,
timestamp: state.metadata.createdAt
};
},
// V2 API (current)
updateState: modernAPI.updateState,
getCurrentState: modernAPI.getCurrentState,
// Version detection
getVersion: () => this.currentVersion
};
}
}
// Usage example
const adapter = new CompatibilityAdapter();
// Migrate old state from chat history
const oldState: WidgetStateV1 = {
data: { message: 'Hello' },
timestamp: 1703462400000
};
const migratedState = adapter.adaptState(oldState, '1.0.0');
console.log(migratedState);
// Output: {
// payload: { message: 'Hello' },
// metadata: {
// createdAt: 1703462400000,
// updatedAt: 1735142400000,
// version: '3.0.0'
// }
// }
The adapter pattern allows you to support multiple API versions simultaneously. When a widget loads from an old chat history, the adapter automatically migrates its state to the current format. This is transparent to both the user and the ChatGPT runtime.
Deprecation policies communicate the lifecycle of features. Establish clear timelines:
- Deprecation announcement (Version X): Feature marked as deprecated, warning logged
- Grace period (Versions X+1, X+2): Feature continues working with warnings
- Removal (Version X+3, major version bump): Feature removed
Always provide migration guides when deprecating features. Users need clear instructions for updating their code.
Creating Automated Migration Paths
Manual migrations are error-prone and time-consuming. Automated migration scripts transform legacy code to match current APIs, dramatically reducing upgrade friction.
// migration-script-generator.ts
interface MigrationRule {
id: string;
description: string;
fromVersion: string;
toVersion: string;
transform: (code: string) => string;
}
class MigrationGenerator {
private rules: MigrationRule[] = [];
registerRule(rule: MigrationRule) {
this.rules.push(rule);
}
generateMigrationScript(fromVersion: string, toVersion: string): string {
const applicableRules = this.rules.filter(
rule => rule.fromVersion === fromVersion && rule.toVersion === toVersion
);
if (applicableRules.length === 0) {
return '// No migration needed';
}
let script = `// Migration from ${fromVersion} to ${toVersion}\n`;
script += `// Generated on ${new Date().toISOString()}\n\n`;
applicableRules.forEach(rule => {
script += `// ${rule.description}\n`;
script += `export function migrate_${rule.id}(code: string): string {\n`;
script += ` return code\n`;
const transform = rule.transform.toString();
script += ` ${transform.split('\n').join('\n ')}\n`;
script += `}\n\n`;
});
return script;
}
applyMigrations(code: string, fromVersion: string, toVersion: string): string {
const applicableRules = this.rules.filter(
rule => rule.fromVersion === fromVersion && rule.toVersion === toVersion
);
return applicableRules.reduce(
(currentCode, rule) => rule.transform(currentCode),
code
);
}
}
// Define migration rules
const migrationGen = new MigrationGenerator();
// V1 → V2: setState → updateState
migrationGen.registerRule({
id: 'setState_to_updateState',
description: 'Replace deprecated setState() with updateState()',
fromVersion: '1.x',
toVersion: '2.x',
transform: (code: string) => {
return code
.replace(/widget\.setState\(/g, 'widget.updateState({ payload: ')
.replace(/\);/g, ' });');
}
});
// V1 → V2: getState → getCurrentState
migrationGen.registerRule({
id: 'getState_to_getCurrentState',
description: 'Replace deprecated getState() with getCurrentState()',
fromVersion: '1.x',
toVersion: '2.x',
transform: (code: string) => {
return code.replace(/widget\.getState\(\)/g, 'widget.getCurrentState().payload');
}
});
// V2 → V3: Async event handlers
migrationGen.registerRule({
id: 'sync_to_async_handlers',
description: 'Convert synchronous event handlers to async',
fromVersion: '2.x',
toVersion: '3.x',
transform: (code: string) => {
// Convert onClick={handler} to onClick={async () => await handler()}
return code.replace(
/onClick=\{([^}]+)\}/g,
'onClick={async () => await $1()}'
);
}
});
// Usage example
const legacyCode = `
widget.setState({ count: 0 });
const currentState = widget.getState();
onClick={incrementCounter}
`;
const migratedCode = migrationGen.applyMigrations(legacyCode, '1.x', '3.x');
console.log(migratedCode);
This migration generator uses AST transformations to automatically update code. For complex migrations, consider using tools like jscodeshift for more robust code transformations.
Client Version Detection and Routing
Runtime version detection enables you to serve appropriate widget versions to different clients. This is crucial when supporting multiple major versions simultaneously during migration periods.
// version-detector.ts
interface ClientInfo {
version: string;
userAgent: string;
capabilities: string[];
}
interface VersionRoute {
pattern: string;
handler: string;
minVersion: string;
maxVersion?: string;
}
class VersionDetector {
private routes: VersionRoute[] = [];
registerRoute(route: VersionRoute) {
this.routes.push(route);
}
detectClientVersion(request: Request): ClientInfo {
// Check version header (preferred)
const versionHeader = request.headers.get('X-Widget-Version');
if (versionHeader) {
return {
version: versionHeader,
userAgent: request.headers.get('User-Agent') || '',
capabilities: this.detectCapabilities(request)
};
}
// Fallback: Parse from User-Agent
const userAgent = request.headers.get('User-Agent') || '';
const versionMatch = userAgent.match(/ChatGPT-Widget\/(\d+\.\d+\.\d+)/);
return {
version: versionMatch ? versionMatch[1] : '1.0.0',
userAgent,
capabilities: this.detectCapabilities(request)
};
}
private detectCapabilities(request: Request): string[] {
const capabilities: string[] = [];
if (request.headers.get('X-Supports-Async')) {
capabilities.push('async-handlers');
}
if (request.headers.get('X-Supports-Dark-Mode')) {
capabilities.push('dark-mode');
}
if (request.headers.get('X-Supports-Streaming')) {
capabilities.push('streaming');
}
return capabilities;
}
routeRequest(clientInfo: ClientInfo, requestPath: string): string | null {
for (const route of this.routes) {
if (!requestPath.match(route.pattern)) continue;
const versionManager = new VersionManager(clientInfo.version);
const minVersion = versionManager.parseVersion(route.minVersion);
const currentVersion = versionManager.parseVersion(clientInfo.version);
if (versionManager.compare(currentVersion, minVersion) < 0) {
continue; // Client version too old
}
if (route.maxVersion) {
const maxVersion = versionManager.parseVersion(route.maxVersion);
if (versionManager.compare(currentVersion, maxVersion) > 0) {
continue; // Client version too new
}
}
return route.handler;
}
return null; // No matching route
}
}
// Usage example
const detector = new VersionDetector();
// Register version-specific routes
detector.registerRoute({
pattern: '/api/widget/render',
handler: 'renderWidgetV1',
minVersion: '1.0.0',
maxVersion: '1.9.9'
});
detector.registerRoute({
pattern: '/api/widget/render',
handler: 'renderWidgetV2',
minVersion: '2.0.0',
maxVersion: '2.9.9'
});
detector.registerRoute({
pattern: '/api/widget/render',
handler: 'renderWidgetV3',
minVersion: '3.0.0'
});
// Handle incoming request
async function handleRequest(request: Request): Promise<Response> {
const clientInfo = detector.detectClientVersion(request);
const handler = detector.routeRequest(clientInfo, '/api/widget/render');
if (!handler) {
return new Response('Unsupported version', { status: 400 });
}
console.log(`Routing to ${handler} for client version ${clientInfo.version}`);
// Invoke appropriate handler...
}
Version detection allows graceful transitions during major version migrations. You can support V2 and V3 clients simultaneously, giving users time to upgrade at their own pace.
Automated Changelog Generation
Manually maintaining changelogs is tedious and error-prone. Automated changelog generation from commit messages ensures accurate, comprehensive release notes.
// changelog-generator.ts
interface Commit {
hash: string;
message: string;
author: string;
timestamp: number;
}
interface ChangelogEntry {
version: string;
date: string;
sections: {
breaking: string[];
features: string[];
fixes: string[];
chores: string[];
};
}
class ChangelogGenerator {
private readonly conventionalCommitPattern = /^(feat|fix|docs|style|refactor|perf|test|chore|breaking)(\(.+\))?:\s*(.+)/;
parseCommit(commit: Commit): { type: string; scope?: string; description: string } | null {
const match = commit.message.match(this.conventionalCommitPattern);
if (!match) return null;
return {
type: match[1],
scope: match[2]?.replace(/[()]/g, ''),
description: match[3]
};
}
generateChangelog(commits: Commit[], version: string): ChangelogEntry {
const entry: ChangelogEntry = {
version,
date: new Date().toISOString().split('T')[0],
sections: {
breaking: [],
features: [],
fixes: [],
chores: []
}
};
commits.forEach(commit => {
const parsed = this.parseCommit(commit);
if (!parsed) return;
const message = parsed.scope
? `**${parsed.scope}**: ${parsed.description}`
: parsed.description;
switch (parsed.type) {
case 'breaking':
entry.sections.breaking.push(message);
break;
case 'feat':
entry.sections.features.push(message);
break;
case 'fix':
entry.sections.fixes.push(message);
break;
case 'chore':
case 'docs':
case 'style':
case 'refactor':
case 'perf':
case 'test':
entry.sections.chores.push(message);
break;
}
});
return entry;
}
formatMarkdown(entry: ChangelogEntry): string {
let markdown = `## [${entry.version}] - ${entry.date}\n\n`;
if (entry.sections.breaking.length > 0) {
markdown += `### ⚠️ BREAKING CHANGES\n\n`;
entry.sections.breaking.forEach(item => {
markdown += `- ${item}\n`;
});
markdown += '\n';
}
if (entry.sections.features.length > 0) {
markdown += `### ✨ Features\n\n`;
entry.sections.features.forEach(item => {
markdown += `- ${item}\n`;
});
markdown += '\n';
}
if (entry.sections.fixes.length > 0) {
markdown += `### 🐛 Bug Fixes\n\n`;
entry.sections.fixes.forEach(item => {
markdown += `- ${item}\n`;
});
markdown += '\n';
}
if (entry.sections.chores.length > 0) {
markdown += `### 🔧 Chores\n\n`;
entry.sections.chores.forEach(item => {
markdown += `- ${item}\n`;
});
markdown += '\n';
}
return markdown;
}
}
// Usage example
const changelogGen = new ChangelogGenerator();
const commits: Commit[] = [
{
hash: 'abc123',
message: 'feat(widget): Add dark mode support',
author: 'developer@example.com',
timestamp: Date.now()
},
{
hash: 'def456',
message: 'fix(state): Resolve memory leak in persistence layer',
author: 'developer@example.com',
timestamp: Date.now()
},
{
hash: 'ghi789',
message: 'breaking(api): Remove deprecated setState method',
author: 'developer@example.com',
timestamp: Date.now()
}
];
const entry = changelogGen.generateChangelog(commits, '2.0.0');
console.log(changelogGen.formatMarkdown(entry));
This changelog generator follows Conventional Commits specification, making it compatible with automated release tools like semantic-release and standard-version.
Deprecation Warning System
Proactive deprecation warnings help users migrate before breaking changes arrive. Implement a warning system that logs deprecations in development and collects telemetry in production.
// deprecation-warner.ts
interface DeprecationWarning {
feature: string;
since: string;
removeIn: string;
replacement?: string;
documentationUrl?: string;
}
class DeprecationWarner {
private warnings: Map<string, DeprecationWarning> = new Map();
private reportedWarnings: Set<string> = new Set();
private isDevelopment: boolean;
constructor(isDevelopment: boolean = false) {
this.isDevelopment = isDevelopment;
}
registerDeprecation(warning: DeprecationWarning) {
this.warnings.set(warning.feature, warning);
}
warn(feature: string, context?: Record<string, any>) {
const warning = this.warnings.get(feature);
if (!warning) {
console.error(`Unknown deprecation: ${feature}`);
return;
}
// Only warn once per feature per session
if (this.reportedWarnings.has(feature)) return;
this.reportedWarnings.add(feature);
const message = this.formatWarning(warning);
if (this.isDevelopment) {
console.warn(message);
if (warning.documentationUrl) {
console.warn(`Documentation: ${warning.documentationUrl}`);
}
} else {
// In production, send to telemetry
this.reportToTelemetry(warning, context);
}
}
private formatWarning(warning: DeprecationWarning): string {
let message = `⚠️ DEPRECATION WARNING: ${warning.feature} has been deprecated since v${warning.since} and will be removed in v${warning.removeIn}.`;
if (warning.replacement) {
message += `\nPlease use ${warning.replacement} instead.`;
}
return message;
}
private reportToTelemetry(warning: DeprecationWarning, context?: Record<string, any>) {
// Send to analytics/monitoring service
fetch('/api/telemetry/deprecation', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
feature: warning.feature,
since: warning.since,
removeIn: warning.removeIn,
context,
timestamp: new Date().toISOString()
})
}).catch(err => {
// Silent fail - don't break user experience
console.error('Failed to report deprecation:', err);
});
}
}
// Usage example
const warner = new DeprecationWarner(process.env.NODE_ENV === 'development');
// Register deprecations
warner.registerDeprecation({
feature: 'setState()',
since: '2.0.0',
removeIn: '3.0.0',
replacement: 'updateState()',
documentationUrl: 'https://docs.example.com/migration/v2-to-v3'
});
warner.registerDeprecation({
feature: 'synchronous event handlers',
since: '2.5.0',
removeIn: '3.0.0',
replacement: 'async event handlers',
documentationUrl: 'https://docs.example.com/async-handlers'
});
// Wrap deprecated methods
function setState(data: any) {
warner.warn('setState()', { caller: 'MyWidget' });
// Still execute the deprecated functionality
return legacySetState(data);
}
Deprecation warnings create awareness without breaking existing code. Users get clear migration paths and timelines, reducing upgrade friction.
Version-Based Routing Strategy
When supporting multiple major versions simultaneously, route requests to appropriate implementations based on client version. This enables zero-downtime migrations.
// version-router.ts
interface VersionedHandler {
version: string;
handler: (request: any) => Promise<any>;
deprecationDate?: Date;
}
class VersionRouter {
private handlers: Map<number, VersionedHandler> = new Map();
private defaultVersion: number;
constructor(defaultVersion: number) {
this.defaultVersion = defaultVersion;
}
registerHandler(majorVersion: number, handler: VersionedHandler) {
this.handlers.set(majorVersion, handler);
}
async route(request: any, clientVersion?: string): Promise<any> {
const version = clientVersion
? parseInt(clientVersion.split('.')[0], 10)
: this.defaultVersion;
const handler = this.handlers.get(version);
if (!handler) {
throw new Error(`No handler registered for version ${version}`);
}
// Check if version is deprecated
if (handler.deprecationDate) {
const daysUntilRemoval = Math.ceil(
(handler.deprecationDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
if (daysUntilRemoval > 0) {
console.warn(
`Version ${version} will be removed in ${daysUntilRemoval} days. Please upgrade.`
);
}
}
return handler.handler(request);
}
getActiveVersions(): number[] {
return Array.from(this.handlers.keys());
}
deprecateVersion(majorVersion: number, removalDate: Date) {
const handler = this.handlers.get(majorVersion);
if (handler) {
handler.deprecationDate = removalDate;
}
}
}
// Usage example
const router = new VersionRouter(3);
// Register V1 handler (deprecated)
router.registerHandler(1, {
version: '1.x',
handler: async (request) => {
return { legacy: true, data: request };
},
deprecationDate: new Date('2026-03-01')
});
// Register V2 handler (stable)
router.registerHandler(2, {
version: '2.x',
handler: async (request) => {
return { stable: true, data: request };
}
});
// Register V3 handler (latest)
router.registerHandler(3, {
version: '3.x',
handler: async (request) => {
return { latest: true, data: request };
}
});
// Route request
async function handleWidgetRequest(request: any, clientVersion?: string) {
try {
const response = await router.route(request, clientVersion);
return response;
} catch (error) {
return { error: 'Unsupported version' };
}
}
Version routing allows graceful migrations. Users on V2 continue working while new users get V3 features. You can deprecate V1, maintain V2, and actively develop V3—all in the same codebase.
Conclusion: Version with Confidence
Effective widget versioning transforms updates from risky deployments into routine improvements. By implementing semantic versioning, maintaining backward compatibility through adapters, providing automated migrations, and routing requests intelligently, you create a sustainable development process that respects existing integrations while enabling innovation.
The patterns demonstrated in this guide—version managers, compatibility adapters, migration generators, version detectors, changelog automation, deprecation warnings, and version routing—form a comprehensive versioning system. Together, they eliminate the fear of breaking changes and empower you to evolve your widgets confidently.
Start with semantic versioning as your foundation. Add compatibility adapters for major version transitions. Implement deprecation warnings early to guide users toward modern APIs. Automate changelog generation to maintain transparent communication. Your users will appreciate the professionalism and reliability.
Ready to implement bulletproof versioning for your ChatGPT widgets? MakeAIHQ.com provides enterprise-grade versioning tools, automated migration scripts, and compatibility testing built into our no-code ChatGPT app builder. Ship updates confidently, maintain backward compatibility effortlessly, and keep your users happy. Start your free trial today and experience professional widget versioning without the complexity.
Internal Links:
- ChatGPT Widget Development Complete Guide
- Widget State Management Advanced Patterns
- Widget Error Boundaries & Recovery Strategies
- MCP Server Deployment Patterns
- CI/CD for ChatGPT Apps: Continuous Testing & Deployment
- ChatGPT App Testing & QA Complete Guide
- ChatGPT App Performance Optimization Complete Guide
- Widget Performance Profiling with Chrome DevTools
- Widget Cross-Browser Compatibility Testing
- ChatGPT App Store Submission Complete Guide
External Links:
Schema Markup:
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "Widget Versioning Best Practices: Semantic Versioning Guide",
"description": "Master widget versioning: semantic versioning, backward compatibility, deprecation strategies, migration guides, and client version management.",
"step": [
{
"@type": "HowToStep",
"name": "Implement Semantic Versioning",
"text": "Use MAJOR.MINOR.PATCH format to communicate breaking changes (major), new features (minor), and bug fixes (patch). Implement a version manager to enforce SemVer rules and generate release notes automatically."
},
{
"@type": "HowToStep",
"name": "Maintain Backward Compatibility",
"text": "Use adapter pattern to translate between old and new APIs. Create compatibility layers that allow legacy integrations to work while you modernize internals. Support multiple API versions during transition periods."
},
{
"@type": "HowToStep",
"name": "Create Automated Migrations",
"text": "Build migration scripts that automatically transform legacy code to match current APIs. Use AST transformations and codemods to reduce manual upgrade work for users."
},
{
"@type": "HowToStep",
"name": "Implement Version Detection",
"text": "Detect client versions at runtime through headers or user agents. Route requests to appropriate handler implementations based on version compatibility requirements."
},
{
"@type": "HowToStep",
"name": "Automate Changelog Generation",
"text": "Parse commit messages following Conventional Commits specification to automatically generate release notes. Categorize changes into breaking, features, fixes, and chores sections."
},
{
"@type": "HowToStep",
"name": "Add Deprecation Warnings",
"text": "Warn users when they use deprecated features. Provide clear replacement recommendations and documentation links. Track deprecation usage through telemetry to plan removal timelines."
},
{
"@type": "HowToStep",
"name": "Route by Version",
"text": "Support multiple major versions simultaneously using version-based routing. Allow gradual migrations by serving appropriate implementations to different client versions."
}
],
"totalTime": "PT2H",
"tool": [
"TypeScript",
"Version Manager",
"Compatibility Adapter",
"Migration Generator"
]
}