API Versioning Strategies

Exercise: API Versioning Strategies

Difficulty - Intermediate

Learning Objectives

  • Implement multiple API versioning strategies
  • Handle backward compatibility gracefully
  • Support version negotiation
  • Build version-aware middleware
  • Manage API deprecation cycles

Problem Statement

Create a comprehensive API versioning system that supports multiple versioning strategies. Your implementation should handle version routing, backward compatibility, deprecation warnings, and seamless migration between API versions.

Requirements

1. URL-Based Versioning

Implement URL path versioning that:

  • Extracts version from URL path
  • Routes requests to version-specific handlers
  • Supports semantic versioning
  • Validates version format
  • Returns 404 for unsupported versions

Example Usage:

 1router := NewVersionedRouter()
 2
 3// Register v1 handler
 4router.HandleVersion("v1", "/users", handleUsersV1)
 5
 6// Register v2 handler
 7router.HandleVersion("v2", "/users", handleUsersV2)
 8
 9// GET /v1/users -> handleUsersV1
10// GET /v2/users -> handleUsersV2

2. Header-Based Versioning

Support version specification via HTTP headers:

  • Reads version from API-Version or Accept-Version header
  • Falls back to default version if header missing
  • Supports custom version header names
  • Validates header format
  • Returns version in response headers

Example Usage:

1handler := NewVersionedHandler()
2handler.RegisterVersion("v1", handleV1)
3handler.RegisterVersion("v2", handleV2)
4handler.SetDefaultVersion("v1")
5
6// Request with header: API-Version: v2
7// Response includes: API-Version: v2

3. Content Negotiation Versioning

Implement version negotiation via Accept header:

  • Parses Accept header media types
  • Extracts version from vendor-specific media types
  • Supports format: application/vnd.company.v2+json
  • Handles quality values
  • Returns appropriate Content-Type in response

Example Usage:

1// Request: Accept: application/vnd.myapi.v2+json
2// Routes to v2 handler
3// Response: Content-Type: application/vnd.myapi.v2+json
4
5negotiator := NewContentNegotiator()
6negotiator.RegisterVersion("v1", "application/vnd.myapi.v1+json", handleV1)
7negotiator.RegisterVersion("v2", "application/vnd.myapi.v2+json", handleV2)

4. Deprecation Management

Create deprecation tracking and warnings:

  • Marks API versions as deprecated
  • Returns Deprecation and Sunset headers
  • Logs usage of deprecated versions
  • Provides migration paths in headers
  • Supports grace periods before removal

Example Usage:

 1manager := NewVersionManager()
 2
 3// Mark v1 as deprecated
 4manager.DeprecateVersion("v1", DeprecationInfo{
 5    DeprecatedAt: time.Now(),
 6    SunsetDate:   time.Now().Add(90 * 24 * time.Hour),
 7    MigrationURL: "https://docs.api.com/migration/v1-to-v2",
 8})
 9
10// Requests to v1 include headers:
11// Deprecation: true
12// Sunset: Sat, 31 Dec 2024 23:59:59 GMT
13// Link: <https://docs.api.com/migration/v1-to-v2>; rel="deprecation"

5. Version Transformation Middleware

Build middleware that transforms requests/responses between versions:

  • Converts v1 request format to v2 format internally
  • Transforms v2 response back to v1 format for client
  • Allows serving multiple versions from single implementation
  • Handles field mapping and renaming
  • Supports bidirectional transformations

Example Usage:

 1transformer := NewVersionTransformer()
 2
 3// Transform v1 request to v2 format
 4transformer.RegisterRequestTransform("v1", "v2", func(req interface{}) interface{} {
 5    v1Req := req.(UserV1)
 6    return UserV2{
 7        ID:        v1Req.ID,
 8        FirstName: splitName(v1Req.Name)[0],
 9        LastName:  splitName(v1Req.Name)[1],
10        Email:     v1Req.Email,
11    }
12})
13
14// Transform v2 response back to v1 format
15transformer.RegisterResponseTransform("v2", "v1", func(resp interface{}) interface{} {
16    v2Resp := resp.(UserV2)
17    return UserV1{
18        ID:    v2Resp.ID,
19        Name:  v2Resp.FirstName + " " + v2Resp.LastName,
20        Email: v2Resp.Email,
21    }
22})

Function Signatures

  1package versioning
  2
  3import (
  4    "net/http"
  5    "time"
  6)
  7
  8// APIVersion represents a version identifier
  9type APIVersion string
 10
 11// VersionedRouter handles URL-based versioning
 12type VersionedRouter struct {
 13    routes map[APIVersion]map[string]http.HandlerFunc
 14}
 15
 16// NewVersionedRouter creates a new versioned router
 17func NewVersionedRouter() *VersionedRouter
 18
 19// HandleVersion registers a handler for a specific version and path
 20func HandleVersion(version APIVersion, path string, handler http.HandlerFunc)
 21
 22// ServeHTTP implements http.Handler
 23func ServeHTTP(w http.ResponseWriter, r *http.Request)
 24
 25// VersionedHandler manages header-based versioning
 26type VersionedHandler struct {
 27    handlers       map[APIVersion]http.HandlerFunc
 28    defaultVersion APIVersion
 29    headerName     string
 30}
 31
 32// NewVersionedHandler creates a new versioned handler
 33func NewVersionedHandler() *VersionedHandler
 34
 35// RegisterVersion registers a handler for a version
 36func RegisterVersion(version APIVersion, handler http.HandlerFunc)
 37
 38// SetDefaultVersion sets the fallback version
 39func SetDefaultVersion(version APIVersion)
 40
 41// SetHeaderName sets the version header name
 42func SetHeaderName(name string)
 43
 44// ServeHTTP implements http.Handler
 45func ServeHTTP(w http.ResponseWriter, r *http.Request)
 46
 47// ContentNegotiator handles Accept header versioning
 48type ContentNegotiator struct {
 49    versions map[APIVersion]MediaTypeHandler
 50}
 51
 52type MediaTypeHandler struct {
 53    MediaType string
 54    Handler   http.HandlerFunc
 55}
 56
 57// NewContentNegotiator creates a new content negotiator
 58func NewContentNegotiator() *ContentNegotiator
 59
 60// RegisterVersion registers a handler for a version and media type
 61func RegisterVersion(version APIVersion, mediaType string, handler http.HandlerFunc)
 62
 63// ServeHTTP implements http.Handler
 64func ServeHTTP(w http.ResponseWriter, r *http.Request)
 65
 66// DeprecationInfo contains deprecation metadata
 67type DeprecationInfo struct {
 68    DeprecatedAt time.Time
 69    SunsetDate   time.Time
 70    MigrationURL string
 71    Message      string
 72}
 73
 74// VersionManager manages API versions and deprecation
 75type VersionManager struct {
 76    versions     map[APIVersion]*VersionInfo
 77    deprecations map[APIVersion]*DeprecationInfo
 78}
 79
 80type VersionInfo struct {
 81    Version     APIVersion
 82    Status      VersionStatus
 83    ReleasedAt  time.Time
 84}
 85
 86type VersionStatus int
 87
 88const (
 89    VersionActive VersionStatus = iota
 90    VersionDeprecated
 91    VersionSunset
 92)
 93
 94// NewVersionManager creates a new version manager
 95func NewVersionManager() *VersionManager
 96
 97// RegisterVersion registers an API version
 98func RegisterVersion(version APIVersion, releasedAt time.Time)
 99
100// DeprecateVersion marks a version as deprecated
101func DeprecateVersion(version APIVersion, info DeprecationInfo)
102
103// IsDeprecated checks if a version is deprecated
104func IsDeprecated(version APIVersion) bool
105
106// GetDeprecationInfo returns deprecation info for a version
107func GetDeprecationInfo(version APIVersion)
108
109// VersionTransformer transforms data between API versions
110type VersionTransformer struct {
111    requestTransforms  map[string]TransformFunc
112    responseTransforms map[string]TransformFunc
113}
114
115type TransformFunc func(interface{}) interface{}
116
117// NewVersionTransformer creates a new version transformer
118func NewVersionTransformer() *VersionTransformer
119
120// RegisterRequestTransform registers a request transformation
121func RegisterRequestTransform(fromVersion, toVersion APIVersion, fn TransformFunc)
122
123// RegisterResponseTransform registers a response transformation
124func RegisterResponseTransform(fromVersion, toVersion APIVersion, fn TransformFunc)
125
126// TransformRequest transforms request data
127func TransformRequest(fromVersion, toVersion APIVersion, data interface{}) interface{}
128
129// TransformResponse transforms response data
130func TransformResponse(fromVersion, toVersion APIVersion, data interface{}) interface{}

Test Cases

Your implementation should pass these test scenarios:

 1// Test URL-based versioning
 2func TestURLVersioning() {
 3    router := NewVersionedRouter()
 4
 5    v1Handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 6        w.Write([]byte("v1"))
 7    })
 8    v2Handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 9        w.Write([]byte("v2"))
10    })
11
12    router.HandleVersion("v1", "/users", v1Handler)
13    router.HandleVersion("v2", "/users", v2Handler)
14
15    // Test v1
16    req := httptest.NewRequest("GET", "/v1/users", nil)
17    w := httptest.NewRecorder()
18    router.ServeHTTP(w, req)
19    assert.Equal(t, "v1", w.Body.String())
20
21    // Test v2
22    req = httptest.NewRequest("GET", "/v2/users", nil)
23    w = httptest.NewRecorder()
24    router.ServeHTTP(w, req)
25    assert.Equal(t, "v2", w.Body.String())
26}
27
28// Test header-based versioning
29func TestHeaderVersioning() {
30    handler := NewVersionedHandler()
31    handler.SetDefaultVersion("v1")
32
33    handler.RegisterVersion("v1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34        w.Write([]byte("v1"))
35    }))
36    handler.RegisterVersion("v2", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37        w.Write([]byte("v2"))
38    }))
39
40    // Request with v2 header
41    req := httptest.NewRequest("GET", "/users", nil)
42    req.Header.Set("API-Version", "v2")
43    w := httptest.NewRecorder()
44    handler.ServeHTTP(w, req)
45
46    assert.Equal(t, "v2", w.Body.String())
47    assert.Equal(t, "v2", w.Header().Get("API-Version"))
48}
49
50// Test deprecation headers
51func TestDeprecationHeaders() {
52    manager := NewVersionManager()
53    sunsetDate := time.Now().Add(30 * 24 * time.Hour)
54
55    manager.RegisterVersion("v1", time.Now().Add(-365*24*time.Hour))
56    manager.DeprecateVersion("v1", DeprecationInfo{
57        DeprecatedAt: time.Now(),
58        SunsetDate:   sunsetDate,
59        MigrationURL: "https://api.com/migrate",
60    })
61
62    assert.True(t, manager.IsDeprecated("v1"))
63
64    info, ok := manager.GetDeprecationInfo("v1")
65    assert.True(t, ok)
66    assert.Equal(t, "https://api.com/migrate", info.MigrationURL)
67}
68
69// Test version transformation
70func TestVersionTransformation() {
71    transformer := NewVersionTransformer()
72
73    transformer.RegisterRequestTransform("v1", "v2", func(data interface{}) interface{} {
74        v1 := data.(map[string]string)
75        return map[string]interface{}{
76            "firstName": v1["name"],
77            "email":     v1["email"],
78        }
79    })
80
81    input := map[string]string{
82        "name":  "John Doe",
83        "email": "john@example.com",
84    }
85
86    result := transformer.TransformRequest("v1", "v2", input)
87
88    v2Data := result.(map[string]interface{})
89    assert.Equal(t, "John Doe", v2Data["firstName"])
90}

Common Pitfalls

⚠️ Watch out for these common mistakes:

  1. Breaking changes: Even in major versions, consider backward compatibility
  2. Missing version in response: Always echo the version used in response headers
  3. No default version: Requests without version should have sensible fallback
  4. Inconsistent versioning: Don't mix versioning strategies without good reason
  5. Premature deprecation: Give users adequate time to migrate
  6. No documentation: Document version differences and migration paths

Hints

💡 Hint 1: Extracting Version from URL

Use path parsing to extract version:

1func extractVersionFromPath(path string) {
2    parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
3    if len(parts) > 0 && strings.HasPrefix(parts[0], "v") {
4        return APIVersion(parts[0]), "/" + strings.Join(parts[1:], "/")
5    }
6    return "", path
7}
💡 Hint 2: Parsing Accept Header

Parse media types from Accept header:

1func parseAcceptHeader(accept string) []MediaType {
2    var mediaTypes []MediaType
3    for _, part := range strings.Split(accept, ",") {
4        mt := parseMediaType(strings.TrimSpace(part))
5        mediaTypes = append(mediaTypes, mt)
6    }
7    return mediaTypes
8}
💡 Hint 3: Deprecation Headers

Add standard deprecation headers to responses:

1func addDeprecationHeaders(w http.ResponseWriter, info *DeprecationInfo) {
2    w.Header().Set("Deprecation", "true")
3    w.Header().Set("Sunset", info.SunsetDate.Format(http.TimeFormat))
4    if info.MigrationURL != "" {
5        w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"deprecation\"", info.MigrationURL))
6    }
7}

Solution

Click to see the solution
  1package versioning
  2
  3import (
  4    "encoding/json"
  5    "fmt"
  6    "net/http"
  7    "strings"
  8    "sync"
  9    "time"
 10)
 11
 12// APIVersion represents a version identifier
 13type APIVersion string
 14
 15// VersionedRouter handles URL-based versioning
 16type VersionedRouter struct {
 17    mu     sync.RWMutex
 18    routes map[APIVersion]map[string]http.HandlerFunc
 19}
 20
 21// NewVersionedRouter creates a new versioned router
 22func NewVersionedRouter() *VersionedRouter {
 23    return &VersionedRouter{
 24        routes: make(map[APIVersion]map[string]http.HandlerFunc),
 25    }
 26}
 27
 28// HandleVersion registers a handler for a specific version and path
 29func HandleVersion(version APIVersion, path string, handler http.HandlerFunc) {
 30    vr.mu.Lock()
 31    defer vr.mu.Unlock()
 32
 33    if vr.routes[version] == nil {
 34        vr.routes[version] = make(map[string]http.HandlerFunc)
 35    }
 36    vr.routes[version][path] = handler
 37}
 38
 39// ServeHTTP implements http.Handler
 40func ServeHTTP(w http.ResponseWriter, r *http.Request) {
 41    version, path := extractVersionFromPath(r.URL.Path)
 42
 43    if version == "" {
 44        http.Error(w, "API version required", http.StatusBadRequest)
 45        return
 46    }
 47
 48    vr.mu.RLock()
 49    versionRoutes, ok := vr.routes[version]
 50    vr.mu.RUnlock()
 51
 52    if !ok {
 53        http.Error(w, fmt.Sprintf("Unsupported API version: %s", version), http.StatusNotFound)
 54        return
 55    }
 56
 57    handler, ok := versionRoutes[path]
 58    if !ok {
 59        http.Error(w, "Not found", http.StatusNotFound)
 60        return
 61    }
 62
 63    // Set version in response header
 64    w.Header().Set("API-Version", string(version))
 65
 66    // Update request path to remove version prefix
 67    r.URL.Path = path
 68    handler(w, r)
 69}
 70
 71func extractVersionFromPath(path string) {
 72    parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
 73    if len(parts) > 0 && strings.HasPrefix(parts[0], "v") {
 74        remainingPath := "/" + strings.Join(parts[1:], "/")
 75        if remainingPath == "/" {
 76            remainingPath = ""
 77        }
 78        return APIVersion(parts[0]), remainingPath
 79    }
 80    return "", path
 81}
 82
 83// VersionedHandler manages header-based versioning
 84type VersionedHandler struct {
 85    mu             sync.RWMutex
 86    handlers       map[APIVersion]http.HandlerFunc
 87    defaultVersion APIVersion
 88    headerName     string
 89}
 90
 91// NewVersionedHandler creates a new versioned handler
 92func NewVersionedHandler() *VersionedHandler {
 93    return &VersionedHandler{
 94        handlers:   make(map[APIVersion]http.HandlerFunc),
 95        headerName: "API-Version",
 96    }
 97}
 98
 99// RegisterVersion registers a handler for a version
100func RegisterVersion(version APIVersion, handler http.HandlerFunc) {
101    vh.mu.Lock()
102    defer vh.mu.Unlock()
103    vh.handlers[version] = handler
104}
105
106// SetDefaultVersion sets the fallback version
107func SetDefaultVersion(version APIVersion) {
108    vh.mu.Lock()
109    defer vh.mu.Unlock()
110    vh.defaultVersion = version
111}
112
113// SetHeaderName sets the version header name
114func SetHeaderName(name string) {
115    vh.mu.Lock()
116    defer vh.mu.Unlock()
117    vh.headerName = name
118}
119
120// ServeHTTP implements http.Handler
121func ServeHTTP(w http.ResponseWriter, r *http.Request) {
122    vh.mu.RLock()
123    headerName := vh.headerName
124    vh.mu.RUnlock()
125
126    // Get version from header
127    version := APIVersion(r.Header.Get(headerName))
128
129    vh.mu.RLock()
130    defaultVersion := vh.defaultVersion
131    vh.mu.RUnlock()
132
133    // Use default if not specified
134    if version == "" {
135        version = defaultVersion
136    }
137
138    vh.mu.RLock()
139    handler, ok := vh.handlers[version]
140    vh.mu.RUnlock()
141
142    if !ok {
143        http.Error(w, fmt.Sprintf("Unsupported API version: %s", version), http.StatusBadRequest)
144        return
145    }
146
147    // Echo version in response
148    w.Header().Set(headerName, string(version))
149    handler(w, r)
150}
151
152// ContentNegotiator handles Accept header versioning
153type ContentNegotiator struct {
154    mu       sync.RWMutex
155    versions map[APIVersion]MediaTypeHandler
156}
157
158type MediaTypeHandler struct {
159    MediaType string
160    Handler   http.HandlerFunc
161}
162
163// NewContentNegotiator creates a new content negotiator
164func NewContentNegotiator() *ContentNegotiator {
165    return &ContentNegotiator{
166        versions: make(map[APIVersion]MediaTypeHandler),
167    }
168}
169
170// RegisterVersion registers a handler for a version and media type
171func RegisterVersion(version APIVersion, mediaType string, handler http.HandlerFunc) {
172    cn.mu.Lock()
173    defer cn.mu.Unlock()
174    cn.versions[version] = MediaTypeHandler{
175        MediaType: mediaType,
176        Handler:   handler,
177    }
178}
179
180// ServeHTTP implements http.Handler
181func ServeHTTP(w http.ResponseWriter, r *http.Request) {
182    accept := r.Header.Get("Accept")
183
184    // Find matching version
185    cn.mu.RLock()
186    defer cn.mu.RUnlock()
187
188    for version, mth := range cn.versions {
189        if strings.Contains(accept, mth.MediaType) {
190            w.Header().Set("Content-Type", mth.MediaType)
191            w.Header().Set("API-Version", string(version))
192            mth.Handler(w, r)
193            return
194        }
195    }
196
197    http.Error(w, "No acceptable media type found", http.StatusNotAcceptable)
198}
199
200// DeprecationInfo contains deprecation metadata
201type DeprecationInfo struct {
202    DeprecatedAt time.Time
203    SunsetDate   time.Time
204    MigrationURL string
205    Message      string
206}
207
208// VersionManager manages API versions and deprecation
209type VersionManager struct {
210    mu           sync.RWMutex
211    versions     map[APIVersion]*VersionInfo
212    deprecations map[APIVersion]*DeprecationInfo
213}
214
215type VersionInfo struct {
216    Version    APIVersion
217    Status     VersionStatus
218    ReleasedAt time.Time
219}
220
221type VersionStatus int
222
223const (
224    VersionActive VersionStatus = iota
225    VersionDeprecated
226    VersionSunset
227)
228
229// NewVersionManager creates a new version manager
230func NewVersionManager() *VersionManager {
231    return &VersionManager{
232        versions:     make(map[APIVersion]*VersionInfo),
233        deprecations: make(map[APIVersion]*DeprecationInfo),
234    }
235}
236
237// RegisterVersion registers an API version
238func RegisterVersion(version APIVersion, releasedAt time.Time) {
239    vm.mu.Lock()
240    defer vm.mu.Unlock()
241    vm.versions[version] = &VersionInfo{
242        Version:    version,
243        Status:     VersionActive,
244        ReleasedAt: releasedAt,
245    }
246}
247
248// DeprecateVersion marks a version as deprecated
249func DeprecateVersion(version APIVersion, info DeprecationInfo) {
250    vm.mu.Lock()
251    defer vm.mu.Unlock()
252
253    if versionInfo, ok := vm.versions[version]; ok {
254        versionInfo.Status = VersionDeprecated
255    }
256    vm.deprecations[version] = &info
257}
258
259// IsDeprecated checks if a version is deprecated
260func IsDeprecated(version APIVersion) bool {
261    vm.mu.RLock()
262    defer vm.mu.RUnlock()
263    _, ok := vm.deprecations[version]
264    return ok
265}
266
267// GetDeprecationInfo returns deprecation info for a version
268func GetDeprecationInfo(version APIVersion) {
269    vm.mu.RLock()
270    defer vm.mu.RUnlock()
271    info, ok := vm.deprecations[version]
272    return info, ok
273}
274
275// AddDeprecationHeaders adds deprecation headers to response
276func AddDeprecationHeaders(w http.ResponseWriter, version APIVersion) {
277    if info, ok := vm.GetDeprecationInfo(version); ok {
278        w.Header().Set("Deprecation", "true")
279        w.Header().Set("Sunset", info.SunsetDate.Format(http.TimeFormat))
280        if info.MigrationURL != "" {
281            w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"deprecation\"", info.MigrationURL))
282        }
283    }
284}
285
286// VersionTransformer transforms data between API versions
287type VersionTransformer struct {
288    mu                 sync.RWMutex
289    requestTransforms  map[string]TransformFunc
290    responseTransforms map[string]TransformFunc
291}
292
293type TransformFunc func(interface{}) interface{}
294
295// NewVersionTransformer creates a new version transformer
296func NewVersionTransformer() *VersionTransformer {
297    return &VersionTransformer{
298        requestTransforms:  make(map[string]TransformFunc),
299        responseTransforms: make(map[string]TransformFunc),
300    }
301}
302
303// RegisterRequestTransform registers a request transformation
304func RegisterRequestTransform(fromVersion, toVersion APIVersion, fn TransformFunc) {
305    vt.mu.Lock()
306    defer vt.mu.Unlock()
307    key := fmt.Sprintf("%s->%s", fromVersion, toVersion)
308    vt.requestTransforms[key] = fn
309}
310
311// RegisterResponseTransform registers a response transformation
312func RegisterResponseTransform(fromVersion, toVersion APIVersion, fn TransformFunc) {
313    vt.mu.Lock()
314    defer vt.mu.Unlock()
315    key := fmt.Sprintf("%s->%s", fromVersion, toVersion)
316    vt.responseTransforms[key] = fn
317}
318
319// TransformRequest transforms request data
320func TransformRequest(fromVersion, toVersion APIVersion, data interface{}) interface{} {
321    vt.mu.RLock()
322    defer vt.mu.RUnlock()
323
324    key := fmt.Sprintf("%s->%s", fromVersion, toVersion)
325    if fn, ok := vt.requestTransforms[key]; ok {
326        return fn(data)
327    }
328    return data
329}
330
331// TransformResponse transforms response data
332func TransformResponse(fromVersion, toVersion APIVersion, data interface{}) interface{} {
333    vt.mu.RLock()
334    defer vt.mu.RUnlock()
335
336    key := fmt.Sprintf("%s->%s", fromVersion, toVersion)
337    if fn, ok := vt.responseTransforms[key]; ok {
338        return fn(data)
339    }
340    return data
341}
342
343// Example usage types
344type UserV1 struct {
345    ID    string `json:"id"`
346    Name  string `json:"name"`
347    Email string `json:"email"`
348}
349
350type UserV2 struct {
351    ID        string `json:"id"`
352    FirstName string `json:"firstName"`
353    LastName  string `json:"lastName"`
354    Email     string `json:"email"`
355}
356
357// Example handlers
358func HandleUsersV1(w http.ResponseWriter, r *http.Request) {
359    user := UserV1{
360        ID:    "123",
361        Name:  "John Doe",
362        Email: "john@example.com",
363    }
364    json.NewEncoder(w).Encode(user)
365}
366
367func HandleUsersV2(w http.ResponseWriter, r *http.Request) {
368    user := UserV2{
369        ID:        "123",
370        FirstName: "John",
371        LastName:  "Doe",
372        Email:     "john@example.com",
373    }
374    json.NewEncoder(w).Encode(user)
375}

Key Takeaways

  • Multiple versioning strategies exist: URL-based, header-based, and content negotiation
  • URL versioning is most visible and cacheable but clutters URLs
  • Header versioning is cleaner but less discoverable
  • Content negotiation follows REST principles but is complex
  • Deprecation headers inform clients about migration
  • Version transformation allows serving multiple versions from single implementation
  • Backward compatibility should be maintained even across major versions when possible
  • Default versions ensure graceful degradation for clients without version specification