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-VersionorAccept-Versionheader - 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
DeprecationandSunsetheaders - 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:
- Breaking changes: Even in major versions, consider backward compatibility
- Missing version in response: Always echo the version used in response headers
- No default version: Requests without version should have sensible fallback
- Inconsistent versioning: Don't mix versioning strategies without good reason
- Premature deprecation: Give users adequate time to migrate
- 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