You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ro...@apache.org on 2019/07/09 03:42:29 UTC

[trafficcontrol] branch master updated: Refactor deliveryservice API minor versioning (#3713)

This is an automated email from the ASF dual-hosted git repository.

rob pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 466684c  Refactor deliveryservice API minor versioning (#3713)
466684c is described below

commit 466684c7580fd6a1963af624416726f9683e60e7
Author: Rawlin Peters <ra...@comcast.com>
AuthorDate: Mon Jul 8 21:42:23 2019 -0600

    Refactor deliveryservice API minor versioning (#3713)
    
    * Refactor deliveryservice API minor versioning
    
    Remove all version-specific logic from the deliveryservices API
    implementation except for the logic of upgrading requests of previous
    minor versions into requests of the latest minor version to be handled
    as such. All main logic now uses and belongs to the "latest" struct, so
    it is no longer confusing as to which methods need updated/called for
    various minor version structs. The versioned structs are now only
    required and used for unmarshalling requests of a specific minor
    version, upgrading those requests to the latest minor version, and
    marshalling responses back down into the requested version. It should be
    much more clear now how to implement and add a new minor version to the
    deliveryservices API (also with updated README instructions).
    
    * Address review comments and remove a couple redundant functions
---
 CHANGELOG.md                                       |   1 +
 lib/go-tc/deliveryservices.go                      | 200 ++++-----
 .../testing/api/v14/deliveryservicematches_test.go |   2 +-
 .../testing/api/v14/deliveryservices_test.go       | 295 ++++++++++++-
 traffic_ops/testing/api/v14/tc-fixtures.json       |  60 +++
 traffic_ops/traffic_ops_golang/README.md           |  75 ++--
 traffic_ops/traffic_ops_golang/api/api.go          |  37 ++
 .../deliveryservice/deliveryservices.go            | 471 ++++++++++++++++-----
 .../deliveryservice/deliveryservicesv12.go         | 191 ---------
 .../deliveryservice/deliveryservicesv13.go         | 135 ------
 .../traffic_ops_golang/deliveryservice/eligible.go |   2 +-
 .../traffic_ops_golang/deliveryservice/urlkey.go   |  10 +-
 traffic_ops/traffic_ops_golang/routing/routes.go   |  15 +-
 13 files changed, 892 insertions(+), 602 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 182e190..070856b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Modified Traffic Router logging format to include an additional field for DNS log entries, namely `rhi`. This defaults to '-' and is only used when EDNS0 client subnet extensions are enabled and a client subnet is present in the request. When enabled and a subnet is present, the subnet appears in the `chi` field and the resolver address is in the `rhi` field.
 - Changed traffic_ops_ort.pl so that hdr_rw-<ds>.config files are compared with strict ordering and line duplication when detecting configuration changes.
 - Traffic Ops (golang), Traffic Monitor, Traffic Stats are now compiled using Go version 1.11. Grove was already being compiled with this version which improves performance for TLS when RSA certificates are used.
+- Fixed issue #3497: TO API clients that don't specify the latest minor version will overwrite/default any fields introduced in later versions
 - Issue 3476: Traffic Router returns partial result for CLIENT_STEERING Delivery Services when Regional Geoblocking or Anonymous Blocking is enabled.
 - Upgraded Traffic Portal to AngularJS 1.7.8
 - Issue 3275: Improved the snapshot diff performance and experience.
diff --git a/lib/go-tc/deliveryservices.go b/lib/go-tc/deliveryservices.go
index 2dcf0cf..296440f 100644
--- a/lib/go-tc/deliveryservices.go
+++ b/lib/go-tc/deliveryservices.go
@@ -13,7 +13,7 @@ import (
 	"github.com/apache/trafficcontrol/lib/go-util"
 
 	"github.com/asaskevich/govalidator"
-	"github.com/go-ozzo/ozzo-validation"
+	validation "github.com/go-ozzo/ozzo-validation"
 )
 
 /*
@@ -43,6 +43,11 @@ type DeliveryServicesResponse struct {
 	Response []DeliveryService `json:"response"`
 }
 
+// DeliveryServicesNullableResponse ...
+type DeliveryServicesNullableResponse struct {
+	Response []DeliveryServiceNullable `json:"response"`
+}
+
 // CreateDeliveryServiceResponse ...
 type CreateDeliveryServiceResponse struct {
 	Response []DeliveryService      `json:"response"`
@@ -61,6 +66,12 @@ type UpdateDeliveryServiceResponse struct {
 	Alerts   []DeliveryServiceAlert `json:"alerts"`
 }
 
+// UpdateDeliveryServiceNullableResponse ...
+type UpdateDeliveryServiceNullableResponse struct {
+	Response []DeliveryServiceNullable `json:"response"`
+	Alerts   []DeliveryServiceAlert    `json:"alerts"`
+}
+
 // DeliveryServiceResponse ...
 type DeliveryServiceResponse struct {
 	Response DeliveryService        `json:"response"`
@@ -75,6 +86,7 @@ type DeleteDeliveryServiceResponse struct {
 type DeliveryService struct {
 	DeliveryServiceV13
 	MaxOriginConnections      int      `json:"maxOriginConnections" db:"max_origin_connections"`
+	ConsistentHashRegex       string   `json:"consistentHashRegex"`
 	ConsistentHashQueryParams []string `json:"consistentHashQueryParams"`
 }
 
@@ -83,7 +95,7 @@ type DeliveryServiceV13 struct {
 	DeepCachingType   DeepCachingType `json:"deepCachingType"`
 	FQPacingRate      int             `json:"fqPacingRate,omitempty"`
 	SigningAlgorithm  string          `json:"signingAlgorithm" db:"signing_algorithm"`
-	Tenant            string          `json:"tenant,omitempty"`
+	Tenant            string          `json:"tenant"`
 	TRRequestHeaders  string          `json:"trRequestHeaders,omitempty"`
 	TRResponseHeaders string          `json:"trResponseHeaders,omitempty"`
 }
@@ -108,7 +120,6 @@ type DeliveryServiceV11 struct {
 	EdgeHeaderRewrite        string                 `json:"edgeHeaderRewrite"`
 	ExampleURLs              []string               `json:"exampleURLs"`
 	GeoLimit                 int                    `json:"geoLimit"`
-	FQPacingRate             int                    `json:"fqPacingRate"`
 	GeoProvider              int                    `json:"geoProvider"`
 	GlobalMaxMBPS            int                    `json:"globalMaxMbps"`
 	GlobalMaxTPS             int                    `json:"globalMaxTps"`
@@ -143,10 +154,12 @@ type DeliveryServiceV11 struct {
 	TypeID                   int                    `json:"typeId"`
 	Type                     DSType                 `json:"type"`
 	TRResponseHeaders        string                 `json:"trResponseHeaders"`
-	TenantID                 int                    `json:"tenantId,omitempty"`
+	TenantID                 int                    `json:"tenantId"`
 	XMLID                    string                 `json:"xmlId"`
 }
 
+type DeliveryServiceNullableV14 DeliveryServiceNullable // this type alias should always alias the latest minor version of the deliveryservices endpoints
+
 type DeliveryServiceNullable struct {
 	DeliveryServiceNullableV13
 	ConsistentHashRegex       *string  `json:"consistentHashRegex"`
@@ -157,7 +170,7 @@ type DeliveryServiceNullable struct {
 type DeliveryServiceNullableV13 struct {
 	DeliveryServiceNullableV12
 	DeepCachingType   *DeepCachingType `json:"deepCachingType" db:"deep_caching_type"`
-	FQPacingRate      *int             `json:"fqPacingRate"`
+	FQPacingRate      *int             `json:"fqPacingRate" db:"fq_pacing_rate"`
 	SigningAlgorithm  *string          `json:"signingAlgorithm" db:"signing_algorithm"`
 	Tenant            *string          `json:"tenant"`
 	TRResponseHeaders *string          `json:"trResponseHeaders"`
@@ -187,7 +200,6 @@ type DeliveryServiceNullableV11 struct {
 	DNSBypassTTL             *int                    `json:"dnsBypassTtl" db:"dns_bypass_ttl"`
 	DSCP                     *int                    `json:"dscp" db:"dscp"`
 	EdgeHeaderRewrite        *string                 `json:"edgeHeaderRewrite" db:"edge_header_rewrite"`
-	FQPacingRate             *int                    `json:"fqPacingRate" db:"fq_pacing_rate"`
 	GeoLimit                 *int                    `json:"geoLimit" db:"geo_limit"`
 	GeoLimitCountries        *string                 `json:"geoLimitCountries" db:"geo_limit_countries"`
 	GeoLimitRedirectURL      *string                 `json:"geoLimitRedirectURL" db:"geolimit_redirect_url"`
@@ -231,42 +243,6 @@ type DeliveryServiceNullableV11 struct {
 	ExampleURLs              []string                `json:"exampleURLs"`
 }
 
-// NewDeliveryServiceNullableFromV12 creates a new V13 DS from a V12 DS, filling new fields with appropriate defaults.
-func NewDeliveryServiceNullableFromV12(ds DeliveryServiceNullableV12) DeliveryServiceNullable {
-	newDSv13 := DeliveryServiceNullableV13{DeliveryServiceNullableV12: ds}
-	newDS := DeliveryServiceNullable{DeliveryServiceNullableV13: newDSv13}
-	newDS.Sanitize()
-	return newDS
-}
-
-// NewDeliveryServiceNullableFromV13 creates a new V14 DS from a V13 DS, filling new fields with appropriate defaults.
-func NewDeliveryServiceNullableFromV13(ds DeliveryServiceNullableV13) DeliveryServiceNullable {
-	newDS := DeliveryServiceNullable{DeliveryServiceNullableV13: ds}
-	newDS.Sanitize()
-	return newDS
-}
-
-func (ds *DeliveryServiceNullableV12) Sanitize() {
-	if ds.GeoLimitCountries != nil {
-		*ds.GeoLimitCountries = strings.ToUpper(strings.Replace(*ds.GeoLimitCountries, " ", "", -1))
-	}
-	if ds.ProfileID != nil && *ds.ProfileID == -1 {
-		ds.ProfileID = nil
-	}
-	if ds.EdgeHeaderRewrite != nil && strings.TrimSpace(*ds.EdgeHeaderRewrite) == "" {
-		ds.EdgeHeaderRewrite = nil
-	}
-	if ds.MidHeaderRewrite != nil && strings.TrimSpace(*ds.MidHeaderRewrite) == "" {
-		ds.MidHeaderRewrite = nil
-	}
-	if ds.RoutingName == nil || *ds.RoutingName == "" {
-		ds.RoutingName = util.StrPtr(DefaultRoutingName)
-	}
-	if ds.AnonymousBlockingEnabled == nil {
-		ds.AnonymousBlockingEnabled = util.BoolPtr(false)
-	}
-}
-
 func requiredIfMatchesTypeName(patterns []string, typeName string) func(interface{}) error {
 	return func(value interface{}) error {
 		switch v := value.(type) {
@@ -302,53 +278,6 @@ func requiredIfMatchesTypeName(patterns []string, typeName string) func(interfac
 	}
 }
 
-func (ds *DeliveryServiceNullableV12) validateTypeFields(tx *sql.Tx) error {
-	// Validate the TypeName related fields below
-	err := error(nil)
-	DNSRegexType := "^DNS.*$"
-	HTTPRegexType := "^HTTP.*$"
-	SteeringRegexType := "^STEERING.*$"
-	latitudeErr := "Must be a floating point number within the range +-90"
-	longitudeErr := "Must be a floating point number within the range +-180"
-
-	typeName, err := ValidateTypeID(tx, ds.TypeID, "deliveryservice")
-	if err != nil {
-		return err
-	}
-
-	errs := validation.Errors{
-		"initialDispersion": validation.Validate(ds.InitialDispersion,
-			validation.By(requiredIfMatchesTypeName([]string{HTTPRegexType}, typeName)),
-			validation.By(tovalidate.IsGreaterThanZero)),
-		"ipv6RoutingEnabled": validation.Validate(ds.IPV6RoutingEnabled,
-			validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
-		"missLat": validation.Validate(ds.MissLat,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
-			validation.Min(-90.0).Error(latitudeErr),
-			validation.Max(90.0).Error(latitudeErr)),
-		"missLong": validation.Validate(ds.MissLong,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
-			validation.Min(-180.0).Error(longitudeErr),
-			validation.Max(180.0).Error(longitudeErr)),
-		"multiSiteOrigin": validation.Validate(ds.MultiSiteOrigin,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-		"orgServerFqdn": validation.Validate(ds.OrgServerFQDN,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
-			validation.NewStringRule(validateOrgServerFQDN, "must start with http:// or https:// and be followed by a valid hostname with an optional port (no trailing slash)")),
-		"protocol": validation.Validate(ds.Protocol,
-			validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
-		"qstringIgnore": validation.Validate(ds.QStringIgnore,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-		"rangeRequestHandling": validation.Validate(ds.RangeRequestHandling,
-			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
-	}
-	toErrs := tovalidate.ToErrors(errs)
-	if len(toErrs) > 0 {
-		return errors.New(util.JoinErrsStr(toErrs))
-	}
-	return nil
-}
-
 func validateOrgServerFQDN(orgServerFQDN string) bool {
 	_, fqdn, port, err := ParseOrgServerFQDN(orgServerFQDN)
 	if err != nil || !govalidator.IsHost(*fqdn) || (port != nil && !govalidator.IsPort(*port)) {
@@ -378,36 +307,25 @@ func ParseOrgServerFQDN(orgServerFQDN string) (*string, *string, *string, error)
 	return &protocol, &FQDN, port, nil
 }
 
-func (ds *DeliveryServiceNullableV12) Validate(tx *sql.Tx) error {
-	ds.Sanitize()
-	isDNSName := validation.NewStringRule(govalidator.IsDNSName, "must be a valid hostname")
-	noPeriods := validation.NewStringRule(tovalidate.NoPeriods, "cannot contain periods")
-	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
-	errs := validation.Errors{
-		"active":              validation.Validate(ds.Active, validation.NotNil),
-		"cdnId":               validation.Validate(ds.CDNID, validation.Required),
-		"displayName":         validation.Validate(ds.DisplayName, validation.Required, validation.Length(1, 48)),
-		"dscp":                validation.Validate(ds.DSCP, validation.NotNil, validation.Min(0)),
-		"geoLimit":            validation.Validate(ds.GeoLimit, validation.NotNil),
-		"geoProvider":         validation.Validate(ds.GeoProvider, validation.NotNil),
-		"logsEnabled":         validation.Validate(ds.LogsEnabled, validation.NotNil),
-		"regionalGeoBlocking": validation.Validate(ds.RegionalGeoBlocking, validation.NotNil),
-		"routingName":         validation.Validate(ds.RoutingName, isDNSName, noPeriods, validation.Length(1, 48)),
-		"typeId":              validation.Validate(ds.TypeID, validation.Required, validation.Min(1)),
-		"xmlId":               validation.Validate(ds.XMLID, noSpaces, noPeriods, validation.Length(1, 48)),
+func (ds *DeliveryServiceNullable) Sanitize() {
+	if ds.GeoLimitCountries != nil {
+		*ds.GeoLimitCountries = strings.ToUpper(strings.Replace(*ds.GeoLimitCountries, " ", "", -1))
 	}
-	toErrs := tovalidate.ToErrors(errs)
-	if err := ds.validateTypeFields(tx); err != nil {
-		toErrs = append(toErrs, errors.New("type fields: "+err.Error()))
+	if ds.ProfileID != nil && *ds.ProfileID == -1 {
+		ds.ProfileID = nil
 	}
-	if len(toErrs) > 0 {
-		return util.JoinErrs(toErrs)
+	if ds.EdgeHeaderRewrite != nil && strings.TrimSpace(*ds.EdgeHeaderRewrite) == "" {
+		ds.EdgeHeaderRewrite = nil
+	}
+	if ds.MidHeaderRewrite != nil && strings.TrimSpace(*ds.MidHeaderRewrite) == "" {
+		ds.MidHeaderRewrite = nil
+	}
+	if ds.RoutingName == nil || *ds.RoutingName == "" {
+		ds.RoutingName = util.StrPtr(DefaultRoutingName)
+	}
+	if ds.AnonymousBlockingEnabled == nil {
+		ds.AnonymousBlockingEnabled = util.BoolPtr(false)
 	}
-	return nil
-}
-
-func (ds *DeliveryServiceNullable) Sanitize() {
-	ds.DeliveryServiceNullableV12.Sanitize()
 	signedAlgorithm := SigningAlgorithmURLSig
 	if ds.Signed && (ds.SigningAlgorithm == nil || *ds.SigningAlgorithm == "") {
 		ds.SigningAlgorithm = &signedAlgorithm
@@ -428,6 +346,11 @@ func (ds *DeliveryServiceNullable) Sanitize() {
 func (ds *DeliveryServiceNullable) validateTypeFields(tx *sql.Tx) error {
 	// Validate the TypeName related fields below
 	err := error(nil)
+	DNSRegexType := "^DNS.*$"
+	HTTPRegexType := "^HTTP.*$"
+	SteeringRegexType := "^STEERING.*$"
+	latitudeErr := "Must be a floating point number within the range +-90"
+	longitudeErr := "Must be a floating point number within the range +-180"
 
 	typeName, err := ValidateTypeID(tx, ds.TypeID, "deliveryservice")
 	if err != nil {
@@ -443,6 +366,30 @@ func (ds *DeliveryServiceNullable) validateTypeFields(tx *sql.Tx) error {
 				}
 				return fmt.Errorf("consistentHashQueryParams not allowed for '%s' deliveryservice type", typeName)
 			})),
+		"initialDispersion": validation.Validate(ds.InitialDispersion,
+			validation.By(requiredIfMatchesTypeName([]string{HTTPRegexType}, typeName)),
+			validation.By(tovalidate.IsGreaterThanZero)),
+		"ipv6RoutingEnabled": validation.Validate(ds.IPV6RoutingEnabled,
+			validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
+		"missLat": validation.Validate(ds.MissLat,
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
+			validation.Min(-90.0).Error(latitudeErr),
+			validation.Max(90.0).Error(latitudeErr)),
+		"missLong": validation.Validate(ds.MissLong,
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
+			validation.Min(-180.0).Error(longitudeErr),
+			validation.Max(180.0).Error(longitudeErr)),
+		"multiSiteOrigin": validation.Validate(ds.MultiSiteOrigin,
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+		"orgServerFqdn": validation.Validate(ds.OrgServerFQDN,
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName)),
+			validation.NewStringRule(validateOrgServerFQDN, "must start with http:// or https:// and be followed by a valid hostname with an optional port (no trailing slash)")),
+		"protocol": validation.Validate(ds.Protocol,
+			validation.By(requiredIfMatchesTypeName([]string{SteeringRegexType, DNSRegexType, HTTPRegexType}, typeName))),
+		"qstringIgnore": validation.Validate(ds.QStringIgnore,
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
+		"rangeRequestHandling": validation.Validate(ds.RangeRequestHandling,
+			validation.By(requiredIfMatchesTypeName([]string{DNSRegexType, HTTPRegexType}, typeName))),
 	}
 	toErrs := tovalidate.ToErrors(errs)
 	if len(toErrs) > 0 {
@@ -455,19 +402,30 @@ func (ds *DeliveryServiceNullable) Validate(tx *sql.Tx) error {
 	ds.Sanitize()
 	neverOrAlways := validation.NewStringRule(tovalidate.IsOneOfStringICase("NEVER", "ALWAYS"),
 		"must be one of 'NEVER' or 'ALWAYS'")
+	isDNSName := validation.NewStringRule(govalidator.IsDNSName, "must be a valid hostname")
+	noPeriods := validation.NewStringRule(tovalidate.NoPeriods, "cannot contain periods")
+	noSpaces := validation.NewStringRule(tovalidate.NoSpaces, "cannot contain spaces")
 	errs := tovalidate.ToErrors(validation.Errors{
-		"deepCachingType": validation.Validate(ds.DeepCachingType, neverOrAlways),
+		"active":              validation.Validate(ds.Active, validation.NotNil),
+		"cdnId":               validation.Validate(ds.CDNID, validation.Required),
+		"deepCachingType":     validation.Validate(ds.DeepCachingType, neverOrAlways),
+		"displayName":         validation.Validate(ds.DisplayName, validation.Required, validation.Length(1, 48)),
+		"dscp":                validation.Validate(ds.DSCP, validation.NotNil, validation.Min(0)),
+		"geoLimit":            validation.Validate(ds.GeoLimit, validation.NotNil),
+		"geoProvider":         validation.Validate(ds.GeoProvider, validation.NotNil),
+		"logsEnabled":         validation.Validate(ds.LogsEnabled, validation.NotNil),
+		"regionalGeoBlocking": validation.Validate(ds.RegionalGeoBlocking, validation.NotNil),
+		"routingName":         validation.Validate(ds.RoutingName, isDNSName, noPeriods, validation.Length(1, 48)),
+		"typeId":              validation.Validate(ds.TypeID, validation.Required, validation.Min(1)),
+		"xmlId":               validation.Validate(ds.XMLID, noSpaces, noPeriods, validation.Length(1, 48)),
 	})
-	if v12Err := ds.DeliveryServiceNullableV12.Validate(tx); v12Err != nil {
-		errs = append(errs, v12Err)
-	}
 	if err := ds.validateTypeFields(tx); err != nil {
 		errs = append(errs, errors.New("type fields: "+err.Error()))
 	}
 	if len(errs) == 0 {
 		return nil
 	}
-	return util.JoinErrs(errs) // don't add context, so versions chain well
+	return util.JoinErrs(errs)
 }
 
 // Value implements the driver.Valuer interface
diff --git a/traffic_ops/testing/api/v14/deliveryservicematches_test.go b/traffic_ops/testing/api/v14/deliveryservicematches_test.go
index 42c8004..cc89256 100644
--- a/traffic_ops/testing/api/v14/deliveryservicematches_test.go
+++ b/traffic_ops/testing/api/v14/deliveryservicematches_test.go
@@ -39,7 +39,7 @@ func GetTestDeliveryServiceMatches(t *testing.T) {
 	}
 
 	for _, ds := range testData.DeliveryServices {
-		if ds.Type == tc.DSTypeAnyMap {
+		if ds.Type == tc.DSTypeAnyMap || len(ds.MatchList) == 0 {
 			continue // ANY_MAP DSes don't require matchLists
 		}
 		if _, ok := dsMatchMap[tc.DeliveryServiceName(ds.XMLID)]; !ok {
diff --git a/traffic_ops/testing/api/v14/deliveryservices_test.go b/traffic_ops/testing/api/v14/deliveryservices_test.go
index 6f73655..0bfe4d5 100644
--- a/traffic_ops/testing/api/v14/deliveryservices_test.go
+++ b/traffic_ops/testing/api/v14/deliveryservices_test.go
@@ -16,6 +16,13 @@ package v14
 */
 
 import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"reflect"
 	"strconv"
 	"testing"
 	"time"
@@ -30,6 +37,7 @@ func TestDeliveryServices(t *testing.T) {
 		UpdateTestDeliveryServices(t)
 		UpdateNullableTestDeliveryServices(t)
 		GetTestDeliveryServices(t)
+		DeliveryServiceMinorVersionsTest(t)
 		DeliveryServiceTenancyTest(t)
 	})
 }
@@ -74,8 +82,8 @@ func GetTestDeliveryServices(t *testing.T) {
 			cnt++
 		}
 	}
-	if cnt > 1 {
-		t.Errorf("exactly 1 deliveryservice should have more than one query param; found %d", cnt)
+	if cnt > 2 {
+		t.Errorf("exactly 2 deliveryservices should have more than one query param; found %d", cnt)
 	}
 }
 
@@ -222,6 +230,289 @@ func DeleteTestDeliveryServices(t *testing.T) {
 	}
 }
 
+func DeliveryServiceMinorVersionsTest(t *testing.T) {
+	testDS := testData.DeliveryServices[4]
+	if testDS.XMLID != "ds-test-minor-versions" {
+		t.Errorf("expected XMLID: ds-test-minor-versions, actual: %s\n", testDS.XMLID)
+	}
+
+	dses, _, err := TOSession.GetDeliveryServicesNullable()
+	if err != nil {
+		t.Errorf("cannot GET DeliveryServices: %v - %v\n", err, dses)
+	}
+	ds := tc.DeliveryServiceNullable{}
+	for _, d := range dses {
+		if *d.XMLID == testDS.XMLID {
+			ds = d
+			break
+		}
+	}
+	// GET latest, verify expected values for 1.3 and 1.4 fields
+	if ds.DeepCachingType == nil {
+		t.Errorf("expected DeepCachingType: %s, actual: nil\n", testDS.DeepCachingType.String())
+	} else if *ds.DeepCachingType != testDS.DeepCachingType {
+		t.Errorf("expected DeepCachingType: %s, actual: %s\n", testDS.DeepCachingType.String(), ds.DeepCachingType.String())
+	}
+	if ds.FQPacingRate == nil {
+		t.Errorf("expected FQPacingRate: %d, actual: nil\n", testDS.FQPacingRate)
+	} else if *ds.FQPacingRate != testDS.FQPacingRate {
+		t.Errorf("expected FQPacingRate: %d, actual: %d\n", testDS.FQPacingRate, *ds.FQPacingRate)
+	}
+	if ds.SigningAlgorithm == nil {
+		t.Errorf("expected SigningAlgorithm: %s, actual: nil\n", testDS.SigningAlgorithm)
+	} else if *ds.SigningAlgorithm != testDS.SigningAlgorithm {
+		t.Errorf("expected SigningAlgorithm: %s, actual: %s\n", testDS.SigningAlgorithm, *ds.SigningAlgorithm)
+	}
+	if ds.Tenant == nil {
+		t.Errorf("expected Tenant: %s, actual: nil\n", testDS.Tenant)
+	} else if *ds.Tenant != testDS.Tenant {
+		t.Errorf("expected Tenant: %s, actual: %s\n", testDS.Tenant, *ds.Tenant)
+	}
+	if ds.TRRequestHeaders == nil {
+		t.Errorf("expected TRRequestHeaders: %s, actual: nil\n", testDS.TRRequestHeaders)
+	} else if *ds.TRRequestHeaders != testDS.TRRequestHeaders {
+		t.Errorf("expected TRRequestHeaders: %s, actual: %s\n", testDS.TRRequestHeaders, *ds.TRRequestHeaders)
+	}
+	if ds.TRResponseHeaders == nil {
+		t.Errorf("expected TRResponseHeaders: %s, actual: nil\n", testDS.TRResponseHeaders)
+	} else if *ds.TRResponseHeaders != testDS.TRResponseHeaders {
+		t.Errorf("expected TRResponseHeaders: %s, actual: %s\n", testDS.TRResponseHeaders, *ds.TRResponseHeaders)
+	}
+	if ds.ConsistentHashRegex == nil {
+		t.Errorf("expected ConsistentHashRegex: %s, actual: nil\n", testDS.ConsistentHashRegex)
+	} else if *ds.ConsistentHashRegex != testDS.ConsistentHashRegex {
+		t.Errorf("expected ConsistentHashRegex: %s, actual: %s\n", testDS.ConsistentHashRegex, *ds.ConsistentHashRegex)
+	}
+	if ds.ConsistentHashQueryParams == nil {
+		t.Errorf("expected ConsistentHashQueryParams: %v, actual: nil\n", testDS.ConsistentHashQueryParams)
+	} else if !reflect.DeepEqual(ds.ConsistentHashQueryParams, testDS.ConsistentHashQueryParams) {
+		t.Errorf("expected ConsistentHashQueryParams: %v, actual: %v\n", testDS.ConsistentHashQueryParams, ds.ConsistentHashQueryParams)
+	}
+	if ds.MaxOriginConnections == nil {
+		t.Errorf("expected MaxOriginConnections: %d, actual: nil\n", testDS.MaxOriginConnections)
+	} else if *ds.MaxOriginConnections != testDS.MaxOriginConnections {
+		t.Errorf("expected MaxOriginConnections: %d, actual: %d\n", testDS.MaxOriginConnections, *ds.MaxOriginConnections)
+	}
+
+	// GET 1.1, verify 1.3 and 1.4 fields are nil
+	data := tc.DeliveryServicesNullableResponse{}
+	if err = makeV11Request(http.MethodGet, "deliveryservices/"+strconv.Itoa(*ds.ID), nil, &data); err != nil {
+		t.Errorf("cannot GET 1.1 deliveryservice: %s\n", err.Error())
+	}
+	respDS := data.Response[0]
+	if !dsV13FieldsAreNil(respDS) || !dsV14FieldsAreNil(respDS) {
+		t.Errorf("expected 1.3 and 1.4 values to be nil, actual: non-nil")
+	}
+
+	// GET 1.3, verify 1.3 fields are non-nil and 1.4 fields are nil
+	data = tc.DeliveryServicesNullableResponse{}
+	if err = makeV13Request(http.MethodGet, "deliveryservices/"+strconv.Itoa(*ds.ID), nil, &data); err != nil {
+		t.Errorf("cannot GET 1.3 deliveryservice: %s\n", err.Error())
+	}
+	respDS = data.Response[0]
+	if dsV13FieldsAreNil(respDS) {
+		t.Errorf("expected 1.3 values to be non-nil, actual: nil\n")
+	}
+	if !dsV14FieldsAreNil(respDS) {
+		t.Errorf("expected 1.4 values to be nil, actual: non-nil")
+	}
+	if _, err = TOSession.DeleteDeliveryService(strconv.Itoa(*ds.ID)); err != nil {
+		t.Errorf("cannot DELETE deliveryservice: %s\n", err.Error())
+	}
+
+	ds.ID = nil
+	dsBody, err := json.Marshal(ds)
+	if err != nil {
+		t.Errorf("cannot POST deliveryservice, failed to marshal JSON: %s\n", err.Error())
+	}
+	dsV11Body, err := json.Marshal(ds.DeliveryServiceNullableV11)
+	if err != nil {
+		t.Errorf("cannot POST deliveryservice, failed to marshal JSON: %s\n", err.Error())
+	}
+
+	// POST 1.3 w/ 1.4 data, verify 1.4 fields were ignored
+	postDSResp := tc.CreateDeliveryServiceNullableResponse{}
+	if err = makeV13Request(http.MethodPost, "deliveryservices", bytes.NewBuffer(dsBody), &postDSResp); err != nil {
+		t.Errorf("cannot POST 1.3 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	if !dsV14FieldsAreNil(postDSResp.Response[0]) {
+		t.Errorf("POST 1.3 expected 1.4 values to be nil, actual: non-nil")
+	}
+	respID := postDSResp.Response[0].ID
+	getDS, _, err := TOSession.GetDeliveryServiceNullable(strconv.Itoa(*respID))
+	if err != nil {
+		t.Errorf("cannot GET deliveryservice: %s\n", err.Error())
+	}
+	if !dsV14FieldsAreNilOrDefault(*getDS) {
+		t.Errorf("POST 1.3 expected 1.4 values to be nil/default, actual: non-nil/default")
+	}
+	if _, err = TOSession.DeleteDeliveryService(strconv.Itoa(*respID)); err != nil {
+		t.Errorf("cannot DELETE deliveryservice: %s\n", err.Error())
+	}
+
+	// POST 1.1 w/ 1.4 data, verify 1.3 and 1.4 fields were ignored
+	postDSResp = tc.CreateDeliveryServiceNullableResponse{}
+	if err = makeV11Request(http.MethodPost, "deliveryservices", bytes.NewBuffer(dsBody), &postDSResp); err != nil {
+		t.Errorf("cannot POST 1.1 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	if !dsV13FieldsAreNil(postDSResp.Response[0]) || !dsV14FieldsAreNil(postDSResp.Response[0]) {
+		t.Errorf("POST 1.1 expected 1.3 and 1.4 values to be nil, actual: non-nil %++v\n", postDSResp.Response[0])
+	}
+	respID = postDSResp.Response[0].ID
+	getDS, _, err = TOSession.GetDeliveryServiceNullable(strconv.Itoa(*respID))
+	if err != nil {
+		t.Errorf("cannot GET deliveryservice: %s\n", err.Error())
+	}
+	if !dsV13FieldsAreNilOrDefault(*getDS) || !dsV14FieldsAreNilOrDefault(*getDS) {
+		t.Errorf("POST 1.1 expected 1.3 and 1.4 values to be nil/default, actual: non-nil/default %++v\n", *getDS)
+	}
+
+	// PUT 1.4 w/ 1.4 data, then verify that a PUT 1.1 with 1.1 data preserves the existing 1.3 and 1.4 data
+	if _, err = TOSession.UpdateDeliveryServiceNullable(strconv.Itoa(*respID), &ds); err != nil {
+		t.Errorf("cannot PUT deliveryservice: %s\n", err.Error())
+	}
+	putDSResp := tc.UpdateDeliveryServiceNullableResponse{}
+	if err = makeV11Request(http.MethodPut, "deliveryservices/"+strconv.Itoa(*respID), bytes.NewBuffer(dsV11Body), &putDSResp); err != nil {
+		t.Errorf("cannot PUT 1.1 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	if !dsV13FieldsAreNil(putDSResp.Response[0]) || !dsV14FieldsAreNil(putDSResp.Response[0]) {
+		t.Errorf("PUT 1.1 expected 1.3 and 1.4 values to be nil, actual: non-nil %++v\n", putDSResp.Response[0])
+	}
+	getDS, _, err = TOSession.GetDeliveryServiceNullable(strconv.Itoa(*respID))
+	if err != nil {
+		t.Errorf("cannot GET deliveryservice: %s\n", err.Error())
+	}
+	if getDS.FQPacingRate == nil {
+		t.Errorf("expected FQPacingRate: %d, actual: nil\n", testDS.FQPacingRate)
+	} else if *getDS.FQPacingRate != testDS.FQPacingRate {
+		t.Errorf("expected FQPacingRate: %d, actual: %d\n", testDS.FQPacingRate, *getDS.FQPacingRate)
+	}
+	if getDS.MaxOriginConnections == nil {
+		t.Errorf("expected MaxOriginConnections: %d, actual: nil\n", testDS.MaxOriginConnections)
+	} else if *getDS.MaxOriginConnections != testDS.MaxOriginConnections {
+		t.Errorf("expected MaxOriginConnections: %d, actual: %d\n", testDS.MaxOriginConnections, *getDS.MaxOriginConnections)
+	}
+
+	// PUT 1.3 w/ 1.1 data, verify that 1.4 fields were preserved
+	putDSResp = tc.UpdateDeliveryServiceNullableResponse{}
+	if err = makeV13Request(http.MethodPut, "deliveryservices/"+strconv.Itoa(*respID), bytes.NewBuffer(dsV11Body), &putDSResp); err != nil {
+		t.Errorf("cannot PUT 1.3 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	if !dsV14FieldsAreNil(putDSResp.Response[0]) {
+		t.Errorf("PUT 1.3 expected 1.4 values to be nil, actual: non-nil %++v\n", putDSResp.Response[0])
+	}
+	getDS, _, err = TOSession.GetDeliveryServiceNullable(strconv.Itoa(*respID))
+	if err != nil {
+		t.Errorf("cannot GET deliveryservice: %s\n", err.Error())
+	}
+	if getDS.MaxOriginConnections == nil {
+		t.Errorf("expected MaxOriginConnections: %d, actual: nil\n", testDS.MaxOriginConnections)
+	} else if *getDS.MaxOriginConnections != testDS.MaxOriginConnections {
+		t.Errorf("expected MaxOriginConnections: %d, actual: %d\n", testDS.MaxOriginConnections, *getDS.MaxOriginConnections)
+	}
+
+	// DELETE+POST 1.1 again, so that 1.3 and 1.4 fields are back to nil/default
+	if _, err = TOSession.DeleteDeliveryService(strconv.Itoa(*respID)); err != nil {
+		t.Errorf("cannot DELETE deliveryservice: %s\n", err.Error())
+	}
+	postDSResp = tc.CreateDeliveryServiceNullableResponse{}
+	if err = makeV11Request(http.MethodPost, "deliveryservices", bytes.NewBuffer(dsV11Body), &postDSResp); err != nil {
+		t.Errorf("cannot POST 1.1 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	respID = postDSResp.Response[0].ID
+
+	// PUT 1.1 w/ 1.4 data - make sure 1.3 and 1.4 fields were ignored
+	putDSResp = tc.UpdateDeliveryServiceNullableResponse{}
+	if err = makeV11Request(http.MethodPut, "deliveryservices/"+strconv.Itoa(*respID), bytes.NewBuffer(dsBody), &putDSResp); err != nil {
+		t.Errorf("cannot PUT 1.1 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	if !dsV13FieldsAreNil(putDSResp.Response[0]) || !dsV14FieldsAreNil(putDSResp.Response[0]) {
+		t.Errorf("PUT 1.1 expected 1.3 and 1.4 values to be nil, actual: non-nil %++v\n", putDSResp.Response[0])
+	}
+	respID = putDSResp.Response[0].ID
+	getDS, _, err = TOSession.GetDeliveryServiceNullable(strconv.Itoa(*respID))
+	if err != nil {
+		t.Errorf("cannot GET deliveryservice: %s\n", err.Error())
+	}
+	if !dsV13FieldsAreNilOrDefault(*getDS) || !dsV14FieldsAreNilOrDefault(*getDS) {
+		t.Errorf("PUT 1.1 expected 1.3 and 1.4 values to be nil/default, actual: non-nil/default %++v\n", *getDS)
+	}
+
+	// PUT 1.3 w/ 1.4 data, make sure 1.4 fields were ignored
+	putDSResp = tc.UpdateDeliveryServiceNullableResponse{}
+	if err = makeV13Request(http.MethodPut, "deliveryservices/"+strconv.Itoa(*respID), bytes.NewBuffer(dsBody), &putDSResp); err != nil {
+		t.Errorf("cannot PUT 1.1 deliveryservice, failed to make request: %s\n", err.Error())
+	}
+	if !dsV14FieldsAreNil(putDSResp.Response[0]) {
+		t.Errorf("PUT 1.3 expected 1.4 values to be nil, actual: non-nil\n")
+	}
+	respID = putDSResp.Response[0].ID
+	getDS, _, err = TOSession.GetDeliveryServiceNullable(strconv.Itoa(*respID))
+	if err != nil {
+		t.Errorf("cannot GET deliveryservice: %s\n", err.Error())
+	}
+	if !dsV14FieldsAreNilOrDefault(*getDS) {
+		t.Errorf("PUT 1.3 expected 1.4 values to be nil/default, actual: non-nil/default\n")
+	}
+}
+
+func dsV13FieldsAreNilOrDefault(ds tc.DeliveryServiceNullable) bool {
+	return (ds.DeepCachingType == nil || *ds.DeepCachingType == tc.DeepCachingTypeNever) &&
+		(ds.FQPacingRate == nil || *ds.FQPacingRate == 0) &&
+		(ds.TRRequestHeaders == nil || *ds.TRRequestHeaders == "") &&
+		(ds.TRResponseHeaders == nil || *ds.TRResponseHeaders == "")
+}
+
+func dsV14FieldsAreNilOrDefault(ds tc.DeliveryServiceNullable) bool {
+	return (ds.ConsistentHashRegex == nil || *ds.ConsistentHashRegex == "") &&
+		(ds.ConsistentHashQueryParams == nil || len(ds.ConsistentHashQueryParams) == 0) &&
+		(ds.MaxOriginConnections == nil || *ds.MaxOriginConnections == 0)
+}
+
+func dsV13FieldsAreNil(ds tc.DeliveryServiceNullable) bool {
+	return ds.DeepCachingType == nil &&
+		ds.FQPacingRate == nil &&
+		ds.SigningAlgorithm == nil &&
+		ds.Tenant == nil &&
+		ds.TRRequestHeaders == nil &&
+		ds.TRResponseHeaders == nil
+}
+
+func dsV14FieldsAreNil(ds tc.DeliveryServiceNullable) bool {
+	return ds.ConsistentHashRegex == nil &&
+		(ds.ConsistentHashQueryParams == nil || len(ds.ConsistentHashQueryParams) == 0) &&
+		ds.MaxOriginConnections == nil
+}
+
+func makeV11Request(method string, path string, body io.Reader, respStruct interface{}) error {
+	return makeRequest("1.1", method, path, body, respStruct)
+}
+
+func makeV13Request(method string, path string, body io.Reader, respStruct interface{}) error {
+	return makeRequest("1.3", method, path, body, respStruct)
+}
+
+// TODO: move this helper function into a better location
+func makeRequest(version string, method string, path string, body io.Reader, respStruct interface{}) error {
+	req, err := http.NewRequest(method, TOSession.URL+"/api/"+version+"/"+path, body)
+	if err != nil {
+		return fmt.Errorf("failed to create request: %s", err.Error())
+	}
+	resp, err := TOSession.Client.Do(req)
+	if err != nil {
+		return fmt.Errorf("running request: %s", err.Error())
+	}
+	defer resp.Body.Close()
+	bts, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("reading body: " + err.Error())
+	}
+	if err = json.Unmarshal(bts, respStruct); err != nil {
+		return fmt.Errorf("unmarshalling body '" + string(bts) + "': " + err.Error())
+	}
+	return nil
+}
+
 func DeliveryServiceTenancyTest(t *testing.T) {
 	dses, _, err := TOSession.GetDeliveryServicesNullable()
 	if err != nil {
diff --git a/traffic_ops/testing/api/v14/tc-fixtures.json b/traffic_ops/testing/api/v14/tc-fixtures.json
index b76a8f4..e0eb4e0 100644
--- a/traffic_ops/testing/api/v14/tc-fixtures.json
+++ b/traffic_ops/testing/api/v14/tc-fixtures.json
@@ -491,6 +491,66 @@
             "type": "ANY_MAP",
             "xmlId": "anymap-ds",
             "anonymousBlockingEnabled": true
+        },
+        {
+            "active": true,
+            "cdnName": "cdn1",
+            "cacheurl": "cacheUrl1",
+            "ccrDnsTtl": 3600,
+            "cdnName": "cdn1",
+            "checkPath": "",
+            "consistentHashQueryParams": ["a", "b", "c"],
+            "consistentHashRegex": "foo",
+            "deepCachingType": "ALWAYS",
+            "displayName": "ds-test-minor-versions",
+            "dnsBypassCname": null,
+            "dnsBypassIp": "",
+            "dnsBypassIp6": "",
+            "dnsBypassTtl": 30,
+            "dscp": 40,
+            "edgeHeaderRewrite": "edgeHeader1",
+            "fqPacingRate": 42,
+            "geoLimit": 0,
+            "geoLimitCountries": "",
+            "geoLimitRedirectURL": null,
+            "geoProvider": 0,
+            "globalMaxMbps": 0,
+            "globalMaxTps": 0,
+            "httpBypassFqdn": "",
+            "infoUrl": "TBD",
+            "initialDispersion": 1,
+            "ipv6RoutingEnabled": true,
+            "logsEnabled": false,
+            "longDesc": "d s 1",
+            "longDesc1": "ds1",
+            "longDesc2": "ds1",
+            "maxDnsAnswers": 0,
+            "maxOriginConnections": 1000,
+            "midHeaderRewrite": "midHeader1",
+            "missLat": 41.881944,
+            "missLong": -87.627778,
+            "multiSiteOrigin": false,
+            "orgServerFqdn": "http://origin-test-minor-version.example.net",
+            "originShield": null,
+            "profileDescription": null,
+            "profileName": null,
+            "protocol": 2,
+            "qstringIgnore": 1,
+            "rangeRequestHandling": 0,
+            "regexRemap": "rr1",
+            "regionalGeoBlocking": false,
+            "remapText": "@plugin=tslua.so @pparam=/opt/trafficserver/etc/trafficserver/remapPlugin1.lua",
+            "routingName": "cdn",
+            "signed": true,
+            "signingAlgorithm": "url_sig",
+            "sslKeyVersion": 2,
+            "tenantId": 1,
+            "tenant": "root",
+            "trRequestHeaders": "X-Foo",
+            "trResponseHeaders": "Access-Control-Allow-Origin: *",
+            "type": "HTTP_LIVE",
+            "xmlId": "ds-test-minor-versions",
+            "anonymousBlockingEnabled": true
         }
     ],
     "divisions": [
diff --git a/traffic_ops/traffic_ops_golang/README.md b/traffic_ops/traffic_ops_golang/README.md
index a368998..4bfb952 100644
--- a/traffic_ops/traffic_ops_golang/README.md
+++ b/traffic_ops/traffic_ops_golang/README.md
@@ -101,7 +101,6 @@ Most structs do not have versioning. If you are adding a field to a struct with
 
 1. In `lib/go-tc`, rename the old struct to be the previous minor version.
     - For example, if you are adding a field to Delivery Service and existing minor version is 1.4 (so your new minor version is 1.5), in `lib/go-tc/deliveryservices.go` rename `type DeliveryServiceNullable struct` to `type DeliveryServiceNullableV14 struct`.
-  - Also rename any `Sanitize` and `Validate` functions to the old object.
 
 2. In `lib/go-tc`, create a new struct with an unversioned name, and anonymously embed the previous struct (that you just renamed), along with your new field.
     - For example:
@@ -112,58 +111,84 @@ type DeliveryServiceNullable struct {
 }
 ```
 
-3. Create a `Sanitize` function on the new struct, e.g. `func (ds *DeliveryServiceNullable) Sanitize()`, which sets your new field to a default value, if it is null.
-    - It must always be possible to create objects with previous API versions. Therefore, this step is not optional.
-    - The new `Sanitize` function must call the previous version's `Sanitize` as well, in order to sanitize all previous versions. E.g.
+3. In `lib/go-tc`, change the struct's type alias to the new minor version.
+    - For example:
+```go
+type DeliveryServiceNullableV15 DeliveryServiceNullable
+```
+
+4. Update the `Sanitize` function on the unversioned struct, e.g. `func (ds *DeliveryServiceNullable) Sanitize()`, which sets your new field to a default value, if it is null.
 ```go
   func (ds *DeliveryServiceNullable) Sanitize() {
-	ds.DeliveryServiceNullableV14.Sanitize()
+    if ds.MyNewField == nil { ... }
 ```
 
-4. Create a `Validate` function, which immediately calls the `Sanitize` function, as well as doing any other validation on your new field.
-    - `Validate` is used to `Sanitize` by the API frameworks. If a `Validate` function doesn't exist, your new field won't be checked and made valid, and may result in nil panics. Therefore, this step is not optional.
+5. Update the `Validate` function on the unversioned struct to add validation for your new field.
     - For example, if your new field is a port, `Validate` should verify it is between 0 and 65535.
     - Almost all fields can be invalid! Don't skip this step. Proper validation is essential to Traffic Control functioning properly and rejecting invalid input.
 
-    For example:
+6. Add new versioned Create and Update handlers for the new version in e.g. `deliveryservice/deliveryservices.go`. The added Create and Update handlers will decode requests into the latest version of the struct and should pass it to an underlying versioned `create` or `update` function:
 
+  For example:
 ```go
-func (ds *DeliveryServiceNullableV14) Validate(tx *sql.Tx) error {
-	ds.Sanitize()
+func CreateV15(w http.ResponseWriter, r *http.Request) {
+  ...
+	ds := tc.DeliveryServiceNullableV15{}
+	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
+		return
+	}
+
+	res, status, userErr, sysErr := createV15(w, r, inf, ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
+		return
+	}
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV15{*res})
+}
+
+func createV15(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS tc.DeliveryServiceNullableV15) *tc.DeliveryServiceNullableV15 {
+  ...
+}
 ```
 
+NOTE: the underlying `create` and `update` functions are chained together so that requests for previous minor versions are upgraded into requests of the next latest version until they are finally handled at the latest minor version.
 
-5. Create a func to convert the previous version to the new latest struct. For example, `func NewDeliveryServiceNullableFromV14(ds DeliveryServiceNullableV14) DeliveryServiceNullable`. This function will typically do nothing more than create the latest object with the older version, and sanitize new fields. E.g.
-```go
-func NewDeliveryServiceNullableFromV14(ds DeliveryServiceNullableV14) DeliveryServiceNullable {
-	newDS := DeliveryServiceNullable{DeliveryServiceNullableV14: ds}
-	newDS.Sanitize()
-	return newDS
-}
+Example call chains:
 ```
+  CreateV12 -> createV12 -> createV13 -> createV14 -> createV15
+  CreateV13         ->      createV13 -> createV14 -> createV15
+  CreateV14                ->            createV14 -> createV15
+  CreateV15                      ->                   createV15
+  ```
 
-6. In `traffic_ops/traffic_ops_golang`, copy the existing previous version file, e.g. `cp traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv1{3,4}.go`.
-    - If the object has no previous version, see `deliveryservice` for an example. The "CRUDer" version file should contain only boilerplate, no logic, and no reference to other versions except the latest. Hence, it should be possible to copy and rename, with no logic changes. The logic and latest version should all be in the main file, e.g. `deliveryservice/deliveryservices.go`.
+In this example you would rename the existing `createV14` function to `createV15` and update its signature to accept and return a V15 struct. Then you would create a new `createV14` function, in which you would simply create a V15 struct, insert the V14 struct into it, and pass it to the `createV15` function. By doing that, the V14 request would essentially be upgraded into a V15 request for the underlying `createV15` handler to use.
 
-7. In the new version file, rename all instances of the previous version to the new version, e.g. `sed -i 's/v13/v14/' deliveryservicesv14.go`.
+For an `updateV14` function, you would follow the same pattern as the create function, but you also have to take into account any existing 1.5 fields that may already exist in the resource. So, you have to read existing 1.5 fields from the DB into your V15 struct before passing it to `updateV15`. That is how an "update" request can be upgraded from a 1.4 request to a 1.5 request.
 
-8. Add the logic for your new field to the latest version file, e.g. `deliveryservice/deliveryservices.go`.
+7. Modify the `createV15` and `updateV15` functions (and associated INSERT and UPDATE SQL queries) to create and update the new field in e.g. `deliveryservice/deliveryservices.go`.
 
-9. Add your new version to `traffic_ops/traffic_ops_golang/routing/routes.go`, and add the versioned object to the previous route.
+8. Modify the `Read` function (and associated SELECT SQL query) to read structs of the new version. For example in `deliveryservice/deliveryservices.go`, you would update the `switch` statement so that `version.Minor >= 5` returns structs of `DeliveryServiceNullable` (the latest version of the struct), and `version.Minor >= 4` returns structs of the embedded `DeliveryServiceNullableV14`. The SELECT SQL query should always be updated to read all of the latest fields, and the `Read` handle [...]
+
+NOTE: the `Delete` handler should not need any modification when adding a new minor version of an API endpoint.
+
+9. Add the routes for your new `CreateV15` and `UpdateV15` handlers to `traffic_ops/traffic_ops_golang/routing/routes.go`.
     - The new latest route must go above the previous version. If the new version is below the old, the new version will never be routed to!
 
     For example, Change:
 ```go
-{1.4, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.4, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV14, auth.PrivLevelOperations, Authenticated, nil},
 ```
 
   To:
 
 ```go
-{1.5, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelReadOnly, Authenticated, nil},
-{1.4, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryServiceV14{}), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.5, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV15, auth.PrivLevelOperations, Authenticated, nil},
+		{1.4, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV14, auth.PrivLevelOperations, Authenticated, nil},
 ```
 
+NOTE: the `Read` and `Delete` handlers should always point to the lowest minor version since they are meant to handle requests of any minor version, so the routes for these handlers should not change when adding a new minor version.
+
 ## Converting Routes to Traffic Ops Golang
 
 Traffic Ops is moving to Go! You can help!
diff --git a/traffic_ops/traffic_ops_golang/api/api.go b/traffic_ops/traffic_ops_golang/api/api.go
index 03577e2..8f21f82 100644
--- a/traffic_ops/traffic_ops_golang/api/api.go
+++ b/traffic_ops/traffic_ops_golang/api/api.go
@@ -287,6 +287,7 @@ type APIInfo struct {
 	IntParams map[string]int
 	User      *auth.CurrentUser
 	ReqID     uint64
+	Version   *Version
 	Tx        *sqlx.Tx
 	Config    *config.Config
 }
@@ -333,6 +334,7 @@ func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) (
 	if err != nil {
 		return &APIInfo{Tx: &sqlx.Tx{}}, errors.New("getting reqID: " + err.Error()), nil, http.StatusInternalServerError
 	}
+	version := getRequestedAPIVersion(r.URL.Path)
 
 	user, err := auth.GetCurrentUser(r.Context())
 	if err != nil {
@@ -350,6 +352,7 @@ func NewInfo(r *http.Request, requiredParams []string, intParamNames []string) (
 	return &APIInfo{
 		Config:    cfg,
 		ReqID:     reqID,
+		Version:   version,
 		Params:    params,
 		IntParams: intParams,
 		User:      user,
@@ -379,6 +382,40 @@ func (val APIInfoImpl) APIInfo() *APIInfo {
 	return val.ReqInfo
 }
 
+type Version struct {
+	Major uint64
+	Minor uint64
+}
+
+// getRequestedAPIVersion returns a pointer to the requested API Version from the request if it exists or returns nil otherwise.
+func getRequestedAPIVersion(path string) *Version {
+	pathParts := strings.Split(path, "/")
+	if len(pathParts) < 2 {
+		return nil // path doesn't start with `/api`, so it's not an api request
+	}
+	if strings.ToLower(pathParts[1]) != "api" {
+		return nil // path doesn't start with `/api`, so it's not an api request
+	}
+	if len(pathParts) < 3 {
+		return nil // path starts with `/api` but not `/api/{version}`, so it's an api request, and an unknown/nonexistent version.
+	}
+	version := pathParts[2]
+
+	versionParts := strings.Split(version, ".")
+	if len(versionParts) != 2 {
+		return nil
+	}
+	majorVersion, err := strconv.ParseUint(versionParts[0], 10, 64)
+	if err != nil {
+		return nil
+	}
+	minorVersion, err := strconv.ParseUint(versionParts[1], 10, 64)
+	if err != nil {
+		return nil
+	}
+	return &Version{Major: majorVersion, Minor: minorVersion}
+}
+
 // GetDB returns the database from the context. This should very rarely be needed, rather `NewInfo` should always be used to get a transaction, except in extenuating circumstances.
 func GetDB(ctx context.Context) (*sqlx.DB, error) {
 	val := ctx.Value(DBContextKey)
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
index ab5c1ef..313cabd 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go
@@ -64,12 +64,38 @@ func (ds *TODeliveryService) SetKeys(keys map[string]interface{}) {
 	ds.ID = &i
 }
 
+func (ds TODeliveryService) GetKeys() (map[string]interface{}, bool) {
+	if ds.ID == nil {
+		return map[string]interface{}{"id": 0}, false
+	}
+	return map[string]interface{}{"id": *ds.ID}, true
+}
+
+func (ds TODeliveryService) GetKeyFieldsInfo() []api.KeyFieldInfo {
+	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
+}
+
+func (ds *TODeliveryService) GetAuditName() string {
+	if ds.XMLID != nil {
+		return *ds.XMLID
+	}
+	return ""
+}
+
+func (ds *TODeliveryService) GetType() string {
+	return "ds"
+}
+
+// IsTenantAuthorized checks that the user is authorized for both the delivery service's existing tenant, and the new tenant they're changing it to (if different).
+func (ds *TODeliveryService) IsTenantAuthorized(user *auth.CurrentUser) (bool, error) {
+	return isTenantAuthorized(ds.ReqInfo, &ds.DeliveryServiceNullable)
+}
+
 func (ds *TODeliveryService) Validate() error {
 	return ds.DeliveryServiceNullable.Validate(ds.APIInfo().Tx.Tx)
 }
 
-// 	TODO allow users to post names (type, cdn, etc) and get the IDs from the names. This isn't trivial to do in a single query, without dynamically building the entire insert query, and ideally inserting would be one query. But it'd be much more convenient for users. Alternatively, remove IDs from the database entirely and use real candidate keys.
-func Create(w http.ResponseWriter, r *http.Request) {
+func CreateV12(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
@@ -77,37 +103,98 @@ func Create(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	ds := tc.DeliveryServiceNullable{}
-	if err := api.Parse(r.Body, inf.Tx.Tx, &ds); err != nil {
+	ds := tc.DeliveryServiceNullableV12{}
+	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
 		return
 	}
 
-	if ds.RoutingName == nil || *ds.RoutingName == "" {
-		ds.RoutingName = util.StrPtr("cdn")
+	res, status, userErr, sysErr := createV12(w, r, inf, ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
+		return
+	}
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV12{*res})
+}
+
+func CreateV13(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	ds := tc.DeliveryServiceNullableV13{}
+	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
+		return
 	}
-	if err := ds.Validate(inf.Tx.Tx); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("invalid request: "+err.Error()), nil)
+
+	res, status, userErr, sysErr := createV13(w, r, inf, ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
-	ds, errCode, userErr, sysErr = create(inf, ds)
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV13{*res})
+}
+
+// 	TODO allow users to post names (type, cdn, etc) and get the IDs from the names. This isn't trivial to do in a single query, without dynamically building the entire insert query, and ideally inserting would be one query. But it'd be much more convenient for users. Alternatively, remove IDs from the database entirely and use real candidate keys.
+func CreateV14(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullable{ds})
+	defer inf.Close()
+
+	ds := tc.DeliveryServiceNullableV14{}
+	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
+		return
+	}
+
+	res, status, userErr, sysErr := createV14(w, r, inf, ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
+		return
+	}
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV14{*res})
+}
+
+func createV12(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS tc.DeliveryServiceNullableV12) (*tc.DeliveryServiceNullableV12, int, error, error) {
+	dsV13 := tc.DeliveryServiceNullableV13{DeliveryServiceNullableV12: reqDS}
+	res, status, userErr, sysErr := createV13(w, r, inf, dsV13)
+	if res != nil {
+		return &res.DeliveryServiceNullableV12, status, userErr, sysErr
+	}
+	return nil, status, userErr, sysErr
+}
+
+func createV13(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS tc.DeliveryServiceNullableV13) (*tc.DeliveryServiceNullableV13, int, error, error) {
+	dsV14 := tc.DeliveryServiceNullableV14{DeliveryServiceNullableV13: reqDS}
+	res, status, userErr, sysErr := createV14(w, r, inf, dsV14)
+	if res != nil {
+		return &res.DeliveryServiceNullableV13, status, userErr, sysErr
+	}
+	return nil, status, userErr, sysErr
 }
 
 // create creates the given ds in the database, and returns the DS with its id and other fields created on insert set. On error, the HTTP status code, user error, and system error are returned. The status code SHOULD NOT be used, if both errors are nil.
-func create(inf *api.APIInfo, ds tc.DeliveryServiceNullable) (tc.DeliveryServiceNullable, int, error, error) {
+func createV14(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS tc.DeliveryServiceNullableV14) (*tc.DeliveryServiceNullableV14, int, error, error) {
+	ds := tc.DeliveryServiceNullable(reqDS)
 	user := inf.User
 	tx := inf.Tx.Tx
 	cfg := inf.Config
 
+	if err := ds.Validate(tx); err != nil {
+		return nil, http.StatusBadRequest, errors.New("invalid request: " + err.Error()), nil
+	}
+
 	if authorized, err := isTenantAuthorized(inf, &ds); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("checking tenant: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("checking tenant: " + err.Error())
 	} else if !authorized {
-		return tc.DeliveryServiceNullable{}, http.StatusForbidden, errors.New("not authorized on this tenant"), nil
+		return nil, http.StatusForbidden, errors.New("not authorized on this tenant"), nil
 	}
 
 	// TODO change DeepCachingType to implement sql.Valuer and sql.Scanner, so sqlx struct scan can be used.
@@ -172,88 +259,126 @@ func create(inf *api.APIInfo, ds tc.DeliveryServiceNullable) (tc.DeliveryService
 
 	if err != nil {
 		usrErr, sysErr, code := api.ParseDBError(err)
-		return tc.DeliveryServiceNullable{}, code, usrErr, sysErr
+		return nil, code, usrErr, sysErr
 	}
 	defer resultRows.Close()
 
 	id := 0
 	lastUpdated := tc.TimeNoMod{}
 	if !resultRows.Next() {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("no deliveryservice request inserted, no id was returned")
+		return nil, http.StatusInternalServerError, nil, errors.New("no deliveryservice request inserted, no id was returned")
 	}
 	if err := resultRows.Scan(&id, &lastUpdated); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("could not scan id from insert: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("could not scan id from insert: " + err.Error())
 	}
 	if resultRows.Next() {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("too many ids returned from deliveryservice request insert")
+		return nil, http.StatusInternalServerError, nil, errors.New("too many ids returned from deliveryservice request insert")
 	}
 	ds.ID = &id
 
 	if ds.ID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing id after insert")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing id after insert")
 	}
 	if ds.XMLID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing xml_id after insert")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing xml_id after insert")
 	}
 	if ds.TypeID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing type after insert")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing type after insert")
 	}
 	dsType, err := getTypeFromID(*ds.TypeID, tx)
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting delivery service type: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("getting delivery service type: " + err.Error())
 	}
 	ds.Type = &dsType
 
 	if err := createDefaultRegex(tx, *ds.ID, *ds.XMLID); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("creating default regex: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("creating default regex: " + err.Error())
 	}
 
 	if c, err := createConsistentHashQueryParams(tx, *ds.ID, ds.ConsistentHashQueryParams); err != nil {
 		usrErr, sysErr, code := api.ParseDBError(err)
-		return tc.DeliveryServiceNullable{}, code, usrErr, sysErr
+		return nil, code, usrErr, sysErr
 	} else {
 		api.CreateChangeLogRawTx(api.ApiChange, fmt.Sprintf("Created %d consistent hash query params for delivery service: %s", c, *ds.XMLID), user, tx)
 	}
 
 	matchlists, err := GetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: " + err.Error())
 	}
 	if matchlist, ok := matchlists[*ds.XMLID]; !ok {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: not found")
+		return nil, http.StatusInternalServerError, nil, errors.New("creating DS: reading matchlists: not found")
 	} else {
 		ds.MatchList = &matchlist
 	}
 
 	cdnName, cdnDomain, dnssecEnabled, err := getCDNNameDomainDNSSecEnabled(*ds.ID, tx)
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("creating DS: getting CDN info: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("creating DS: getting CDN info: " + err.Error())
 	}
 
 	ds.ExampleURLs = MakeExampleURLs(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain)
 
 	if err := EnsureParams(tx, *ds.ID, *ds.XMLID, ds.EdgeHeaderRewrite, ds.MidHeaderRewrite, ds.RegexRemap, ds.CacheURL, ds.SigningAlgorithm, dsType, ds.MaxOriginConnections); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("ensuring ds parameters:: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("ensuring ds parameters:: " + err.Error())
 	}
 
 	if dnssecEnabled {
 		if err := PutDNSSecKeys(tx, cfg, *ds.XMLID, cdnName, ds.ExampleURLs); err != nil {
-			return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys: " + err.Error())
+			return nil, http.StatusInternalServerError, nil, errors.New("creating DNSSEC keys: " + err.Error())
 		}
 	}
 
 	if err := createPrimaryOrigin(tx, user, ds); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("creating delivery service: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("creating delivery service: " + err.Error())
 	}
 
 	ds.LastUpdated = &lastUpdated
 	if err := api.CreateChangeLogRawErr(api.ApiChange, "Created ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), user, tx); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("error writing to audit log: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("error writing to audit log: " + err.Error())
+	}
+
+	dsLatest := tc.DeliveryServiceNullableV14(ds)
+	return &dsLatest, http.StatusOK, nil, nil
+}
+
+func createDefaultRegex(tx *sql.Tx, dsID int, xmlID string) error {
+	regexStr := `.*\.` + xmlID + `\..*`
+	regexID := 0
+	if err := tx.QueryRow(`INSERT INTO regex (type, pattern) VALUES ((select id from type where name = 'HOST_REGEXP'), $1::text) RETURNING id`, regexStr).Scan(&regexID); err != nil {
+		return errors.New("insert regex: " + err.Error())
+	}
+	if _, err := tx.Exec(`INSERT INTO deliveryservice_regex (deliveryservice, regex, set_number) VALUES ($1::bigint, $2::bigint, 0)`, dsID, regexID); err != nil {
+		return errors.New("executing parameter query to insert location: " + err.Error())
+	}
+	return nil
+}
+
+func createConsistentHashQueryParams(tx *sql.Tx, dsID int, consistentHashQueryParams []string) (int, error) {
+	if len(consistentHashQueryParams) == 0 {
+		return 0, nil
+	}
+	c := 0
+	q := `INSERT INTO deliveryservice_consistent_hash_query_param (name, deliveryservice_id) VALUES ($1, $2)`
+	for _, k := range consistentHashQueryParams {
+		if _, err := tx.Exec(q, k, dsID); err != nil {
+			return c, err
+		}
+		c++
 	}
-	return ds, http.StatusOK, nil, nil
+
+	return c, nil
 }
 
 func (ds *TODeliveryService) Read() ([]interface{}, error, error, int) {
+	version := ds.APIInfo().Version
+	if version == nil {
+		return nil, nil, errors.New("TODeliveryService.Read called with nil API version"), http.StatusInternalServerError
+	}
+	if version.Major != 1 || version.Minor < 1 {
+		return nil, nil, fmt.Errorf("TODeliveryService.Read called with invalid API version: %d.%d", version.Major, version.Minor), http.StatusInternalServerError
+	}
+
 	returnable := []interface{}{}
 	dses, errs, _ := readGetDeliveryServices(ds.APIInfo().Params, ds.APIInfo().Tx, ds.APIInfo().User)
 	if len(errs) > 0 {
@@ -266,12 +391,22 @@ func (ds *TODeliveryService) Read() ([]interface{}, error, error, int) {
 	}
 
 	for _, ds := range dses {
-		returnable = append(returnable, ds)
+		switch {
+		// NOTE: it's required to handle minor version cases in a descending >= manner
+		case version.Minor >= 4:
+			returnable = append(returnable, ds)
+		case version.Minor >= 3:
+			returnable = append(returnable, ds.DeliveryServiceNullableV13)
+		case version.Minor >= 1:
+			returnable = append(returnable, ds.DeliveryServiceNullableV12)
+		default:
+			return nil, nil, fmt.Errorf("TODeliveryService.Read called with invalid API version: %d.%d", version.Major, version.Minor), http.StatusInternalServerError
+		}
 	}
 	return returnable, nil, nil, http.StatusOK
 }
 
-func Update(w http.ResponseWriter, r *http.Request) {
+func UpdateV12(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
@@ -281,78 +416,169 @@ func Update(w http.ResponseWriter, r *http.Request) {
 
 	id := inf.IntParams["id"]
 
-	ds := tc.DeliveryServiceNullable{}
+	ds := tc.DeliveryServiceNullableV12{}
 	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
 		return
 	}
 	ds.ID = &id
 
-	if err := ds.Validate(inf.Tx.Tx); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("invalid request: "+err.Error()), nil)
+	res, status, userErr, sysErr := updateV12(w, r, inf, &ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
 		return
 	}
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV12{*res})
+}
 
-	ds, errCode, userErr, sysErr = update(inf, &ds)
+func UpdateV13(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullable{ds})
-}
+	defer inf.Close()
 
-func createDefaultRegex(tx *sql.Tx, dsID int, xmlID string) error {
-	regexStr := `.*\.` + xmlID + `\..*`
-	regexID := 0
-	if err := tx.QueryRow(`INSERT INTO regex (type, pattern) VALUES ((select id from type where name = 'HOST_REGEXP'), $1::text) RETURNING id`, regexStr).Scan(&regexID); err != nil {
-		return errors.New("insert regex: " + err.Error())
+	id := inf.IntParams["id"]
+
+	ds := tc.DeliveryServiceNullableV13{}
+	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
+		return
 	}
-	if _, err := tx.Exec(`INSERT INTO deliveryservice_regex (deliveryservice, regex, set_number) VALUES ($1::bigint, $2::bigint, 0)`, dsID, regexID); err != nil {
-		return errors.New("executing parameter query to insert location: " + err.Error())
+	ds.ID = &id
+
+	res, status, userErr, sysErr := updateV13(w, r, inf, &ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
+		return
 	}
-	return nil
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV13{*res})
 }
 
-func createConsistentHashQueryParams(tx *sql.Tx, dsID int, consistentHashQueryParams []string) (int, error) {
-	if len(consistentHashQueryParams) == 0 {
-		return 0, nil
+func UpdateV14(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
 	}
-	c := 0
-	q := `INSERT INTO deliveryservice_consistent_hash_query_param (name, deliveryservice_id) VALUES ($1, $2)`
-	for _, k := range consistentHashQueryParams {
-		if _, err := tx.Exec(q, k, dsID); err != nil {
-			return c, err
+	defer inf.Close()
+
+	id := inf.IntParams["id"]
+
+	ds := tc.DeliveryServiceNullableV14{}
+	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
+		return
+	}
+	ds.ID = &id
+
+	res, status, userErr, sysErr := updateV14(w, r, inf, &ds)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr)
+		return
+	}
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV14{*res})
+}
+
+func updateV12(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS *tc.DeliveryServiceNullableV12) (*tc.DeliveryServiceNullableV12, int, error, error) {
+	dsV13 := tc.DeliveryServiceNullableV13{DeliveryServiceNullableV12: *reqDS}
+	// query the DB for existing 1.3 fields in order to "upgrade" this 1.2 request into a 1.3 request
+	query := `
+SELECT
+  ds.deep_caching_type,
+  ds.fq_pacing_rate,
+  ds.signing_algorithm,
+  ds.tr_response_headers,
+  ds.tr_request_headers
+FROM
+  deliveryservice ds
+WHERE
+  ds.id = $1`
+	if err := inf.Tx.Tx.QueryRow(query, *reqDS.ID).Scan(
+		&dsV13.DeepCachingType,
+		&dsV13.FQPacingRate,
+		&dsV13.SigningAlgorithm,
+		&dsV13.TRResponseHeaders,
+		&dsV13.TRRequestHeaders,
+	); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, http.StatusNotFound, fmt.Errorf("delivery service ID %d not found", *dsV13.ID), nil
 		}
-		c++
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("querying delivery service ID %d: %s", *dsV13.ID, err.Error())
+	}
+	if dsV13.DeepCachingType != nil {
+		*dsV13.DeepCachingType = tc.DeepCachingTypeFromString(string(*dsV13.DeepCachingType))
 	}
 
-	return c, nil
+	res, status, userErr, sysErr := updateV13(w, r, inf, &dsV13)
+	if res != nil {
+		return &res.DeliveryServiceNullableV12, status, userErr, sysErr
+	}
+	return nil, status, userErr, sysErr
 }
 
-func update(inf *api.APIInfo, ds *tc.DeliveryServiceNullable) (tc.DeliveryServiceNullable, int, error, error) {
+func updateV13(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS *tc.DeliveryServiceNullableV13) (*tc.DeliveryServiceNullableV13, int, error, error) {
+	dsV14 := tc.DeliveryServiceNullableV14{DeliveryServiceNullableV13: *reqDS}
+	// query the DB for existing 1.4 fields in order to "upgrade" this 1.3 request into a 1.4 request
+	query := `
+SELECT
+  ds.consistent_hash_regex,
+  ds.max_origin_connections,
+  (SELECT ARRAY_AGG(name ORDER BY name)
+    FROM deliveryservice_consistent_hash_query_param
+    WHERE deliveryservice_id = ds.id) AS query_keys
+FROM
+  deliveryservice ds
+WHERE
+  ds.id = $1`
+	if err := inf.Tx.Tx.QueryRow(query, *reqDS.ID).Scan(
+		&dsV14.ConsistentHashRegex,
+		&dsV14.MaxOriginConnections,
+		pq.Array(&dsV14.ConsistentHashQueryParams),
+	); err != nil {
+		if err == sql.ErrNoRows {
+			return nil, http.StatusNotFound, fmt.Errorf("delivery service ID %d not found", *dsV14.ID), nil
+		}
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("querying delivery service ID %d: %s", *dsV14.ID, err.Error())
+	}
+	res, status, userErr, sysErr := updateV14(w, r, inf, &dsV14)
+	if res != nil {
+		return &res.DeliveryServiceNullableV13, status, userErr, sysErr
+	}
+	return nil, status, userErr, sysErr
+}
+
+func updateV14(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, reqDS *tc.DeliveryServiceNullableV14) (*tc.DeliveryServiceNullableV14, int, error, error) {
+	converted := tc.DeliveryServiceNullable(*reqDS)
+	ds := &converted
 	tx := inf.Tx.Tx
 	cfg := inf.Config
 	user := inf.User
 
+	if err := ds.Validate(tx); err != nil {
+		return nil, http.StatusBadRequest, errors.New("invalid request: " + err.Error()), nil
+	}
+
 	if authorized, err := isTenantAuthorized(inf, ds); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("checking tenant: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("checking tenant: " + err.Error())
 	} else if !authorized {
-		return tc.DeliveryServiceNullable{}, http.StatusForbidden, errors.New("not authorized on this tenant"), nil
+		return nil, http.StatusForbidden, errors.New("not authorized on this tenant"), nil
 	}
 
 	if ds.XMLID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusBadRequest, errors.New("missing xml_id"), nil
+		return nil, http.StatusBadRequest, errors.New("missing xml_id"), nil
 	}
 	if ds.ID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusBadRequest, errors.New("missing id"), nil
+		return nil, http.StatusBadRequest, errors.New("missing id"), nil
 	}
 
 	dsType, ok, err := getDSType(tx, *ds.XMLID)
 	if !ok {
-		return tc.DeliveryServiceNullable{}, http.StatusNotFound, errors.New("delivery service '" + *ds.XMLID + "' not found"), nil
+		return nil, http.StatusNotFound, errors.New("delivery service '" + *ds.XMLID + "' not found"), nil
 	}
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting delivery service type during update: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("getting delivery service type during update: " + err.Error())
 	}
 
 	// oldHostName will be used to determine if SSL Keys need updating - this will be empty if the DS doesn't have SSL keys, because DS types without SSL keys may not have regexes, and thus will fail to get a host name.
@@ -360,7 +586,7 @@ func update(inf *api.APIInfo, ds *tc.DeliveryServiceNullable) (tc.DeliveryServic
 	if dsType.HasSSLKeys() {
 		oldHostName, err = getOldHostName(*ds.ID, tx)
 		if err != nil {
-			return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting existing delivery service hostname: " + err.Error())
+			return nil, http.StatusInternalServerError, nil, errors.New("getting existing delivery service hostname: " + err.Error())
 		}
 	}
 
@@ -427,53 +653,53 @@ func update(inf *api.APIInfo, ds *tc.DeliveryServiceNullable) (tc.DeliveryServic
 
 	if err != nil {
 		usrErr, sysErr, code := api.ParseDBError(err)
-		return tc.DeliveryServiceNullable{}, code, usrErr, sysErr
+		return nil, code, usrErr, sysErr
 	}
 	defer resultRows.Close()
 	if !resultRows.Next() {
-		return tc.DeliveryServiceNullable{}, http.StatusNotFound, errors.New("no delivery service found with this id"), nil
+		return nil, http.StatusNotFound, errors.New("no delivery service found with this id"), nil
 	}
 	lastUpdated := tc.TimeNoMod{}
 	if err := resultRows.Scan(&lastUpdated); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("scan updating delivery service: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("scan updating delivery service: " + err.Error())
 	}
 	if resultRows.Next() {
 		xmlID := ""
 		if ds.XMLID != nil {
 			xmlID = *ds.XMLID
 		}
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("updating delivery service " + xmlID + ": " + "this update affected too many rows: > 1")
+		return nil, http.StatusInternalServerError, nil, errors.New("updating delivery service " + xmlID + ": " + "this update affected too many rows: > 1")
 	}
 
 	if ds.ID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing id after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing id after update")
 	}
 	if ds.XMLID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing xml_id after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing xml_id after update")
 	}
 	if ds.TypeID == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing type after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing type after update")
 	}
 	if ds.RoutingName == nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("missing routing name after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("missing routing name after update")
 	}
 	newDSType, err := getTypeFromID(*ds.TypeID, tx)
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting delivery service type after update: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("getting delivery service type after update: " + err.Error())
 	}
 	ds.Type = &newDSType
 
 	cdnDomain, err := getCDNDomain(*ds.ID, tx) // need to get the domain again, in case it changed.
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting CDN domain after update: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("getting CDN domain after update: " + err.Error())
 	}
 
 	matchLists, err := GetDeliveryServicesMatchLists([]string{*ds.XMLID}, tx)
 	if err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting matchlists after update: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("getting matchlists after update: " + err.Error())
 	}
 	if ml, ok := matchLists[*ds.XMLID]; !ok {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("no matchlists after update")
+		return nil, http.StatusInternalServerError, nil, errors.New("no matchlists after update")
 	} else {
 		ds.MatchList = &ml
 	}
@@ -483,22 +709,22 @@ func update(inf *api.APIInfo, ds *tc.DeliveryServiceNullable) (tc.DeliveryServic
 	if dsType.HasSSLKeys() {
 		newHostName, err = getHostName(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain)
 		if err != nil {
-			return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("getting hostname after update: " + err.Error())
+			return nil, http.StatusInternalServerError, nil, errors.New("getting hostname after update: " + err.Error())
 		}
 	}
 
 	if newDSType.HasSSLKeys() && oldHostName != newHostName {
 		if err := updateSSLKeys(ds, newHostName, tx, cfg); err != nil {
-			return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("updating delivery service " + *ds.XMLID + ": updating SSL keys: " + err.Error())
+			return nil, http.StatusInternalServerError, nil, errors.New("updating delivery service " + *ds.XMLID + ": updating SSL keys: " + err.Error())
 		}
 	}
 
 	if err := EnsureParams(tx, *ds.ID, *ds.XMLID, ds.EdgeHeaderRewrite, ds.MidHeaderRewrite, ds.RegexRemap, ds.CacheURL, ds.SigningAlgorithm, newDSType, ds.MaxOriginConnections); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("ensuring ds parameters:: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("ensuring ds parameters:: " + err.Error())
 	}
 
 	if err := updatePrimaryOrigin(tx, user, *ds); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("updating delivery service: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("updating delivery service: " + err.Error())
 	}
 
 	ds.LastUpdated = &lastUpdated
@@ -506,22 +732,69 @@ func update(inf *api.APIInfo, ds *tc.DeliveryServiceNullable) (tc.DeliveryServic
 	// the update may change or delete the query params -- delete existing and re-add if any provided
 	q := `DELETE FROM deliveryservice_consistent_hash_query_param WHERE deliveryservice_id = $1`
 	if res, err := tx.Exec(q, *ds.ID); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, fmt.Errorf("deleting consistent hash query params for ds %s: %s", *ds.XMLID, err.Error())
+		return nil, http.StatusInternalServerError, nil, fmt.Errorf("deleting consistent hash query params for ds %s: %s", *ds.XMLID, err.Error())
 	} else if c, _ := res.RowsAffected(); c > 0 {
 		api.CreateChangeLogRawTx(api.ApiChange, fmt.Sprintf("Deleted %d consistent hash query params for delivery service: %s", c, *ds.XMLID), user, tx)
 	}
 
 	if c, err := createConsistentHashQueryParams(tx, *ds.ID, ds.ConsistentHashQueryParams); err != nil {
 		usrErr, sysErr, code := api.ParseDBError(err)
-		return tc.DeliveryServiceNullable{}, code, usrErr, sysErr
+		return nil, code, usrErr, sysErr
 	} else {
 		api.CreateChangeLogRawTx(api.ApiChange, fmt.Sprintf("Created %d consistent hash query params for delivery service: %s", c, *ds.XMLID), user, tx)
 	}
 
 	if err := api.CreateChangeLogRawErr(api.ApiChange, "Updated ds: "+*ds.XMLID+" id: "+strconv.Itoa(*ds.ID), user, tx); err != nil {
-		return tc.DeliveryServiceNullable{}, http.StatusInternalServerError, nil, errors.New("writing change log entry: " + err.Error())
+		return nil, http.StatusInternalServerError, nil, errors.New("writing change log entry: " + err.Error())
 	}
-	return *ds, http.StatusOK, nil, nil
+	dsLatest := tc.DeliveryServiceNullableV14(*ds)
+	return &dsLatest, http.StatusOK, nil, nil
+}
+
+//Delete is the DeliveryService implementation of the Deleter interface.
+func (ds *TODeliveryService) Delete() (error, error, int) {
+	if ds.ID == nil {
+		return errors.New("missing id"), nil, http.StatusBadRequest
+	}
+
+	xmlID, ok, err := GetXMLID(ds.ReqInfo.Tx.Tx, *ds.ID)
+	if err != nil {
+		return nil, errors.New("ds delete: getting xmlid: " + err.Error()), http.StatusInternalServerError
+	} else if !ok {
+		return errors.New("delivery service not found"), nil, http.StatusNotFound
+	}
+	ds.XMLID = &xmlID
+
+	// Note ds regexes MUST be deleted before the ds, because there's a ON DELETE CASCADE on deliveryservice_regex (but not on regex).
+	// Likewise, it MUST happen in a transaction with the later DS delete, so they aren't deleted if the DS delete fails.
+	if _, err := ds.ReqInfo.Tx.Tx.Exec(`DELETE FROM regex WHERE id IN (SELECT regex FROM deliveryservice_regex WHERE deliveryservice=$1)`, *ds.ID); err != nil {
+		return nil, errors.New("TODeliveryService.Delete deleting regexes for delivery service: " + err.Error()), http.StatusInternalServerError
+	}
+
+	if _, err := ds.ReqInfo.Tx.Tx.Exec(`DELETE FROM deliveryservice_regex WHERE deliveryservice=$1`, *ds.ID); err != nil {
+		return nil, errors.New("TODeliveryService.Delete deleting delivery service regexes: " + err.Error()), http.StatusInternalServerError
+	}
+
+	userErr, sysErr, errCode := api.GenericDelete(ds)
+	if userErr != nil || sysErr != nil {
+		return userErr, sysErr, errCode
+	}
+
+	paramConfigFilePrefixes := []string{"hdr_rw_", "hdr_rw_mid_", "regex_remap_", "cacheurl_"}
+	configFiles := []string{}
+	for _, prefix := range paramConfigFilePrefixes {
+		configFiles = append(configFiles, prefix+*ds.XMLID+".config")
+	}
+
+	if _, err := ds.ReqInfo.Tx.Tx.Exec(`DELETE FROM parameter WHERE name = 'location' AND config_file = ANY($1)`, pq.Array(configFiles)); err != nil {
+		return nil, errors.New("TODeliveryService.Delete deleting delivery service parameteres: " + err.Error()), http.StatusInternalServerError
+	}
+
+	return nil, nil, http.StatusOK
+}
+
+func (v *TODeliveryService) DeleteQuery() string {
+	return `DELETE FROM deliveryservice WHERE id = :id`
 }
 
 func readGetDeliveryServices(params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser) ([]tc.DeliveryServiceNullable, []error, tc.ApiErrorType) {
@@ -1138,7 +1411,7 @@ func getTenantID(tx *sql.Tx, ds *tc.DeliveryServiceNullable) (*int, error) {
 		existingID, _, err := getDSTenantIDByID(tx, *ds.ID) // ignore exists return - if the DS is new, we only need to check the user input tenant
 		return existingID, err
 	}
-	existingID, _, err := getDSTenantIDByName(tx, *ds.XMLID) // ignore exists return - if the DS is new, we only need to check the user input tenant
+	existingID, _, err := getDSTenantIDByName(tx, tc.DeliveryServiceName(*ds.XMLID)) // ignore exists return - if the DS is new, we only need to check the user input tenant
 	return existingID, err
 }
 
@@ -1186,32 +1459,8 @@ func getDSTenantIDByID(tx *sql.Tx, id int) (*int, bool, error) {
 	return tenantID, true, nil
 }
 
-// GetDSTenantIDByIDTx returns the tenant ID, whether the delivery service exists, and any error.
-func GetDSTenantIDByIDTx(tx *sql.Tx, id int) (*int, bool, error) {
-	tenantID := (*int)(nil)
-	if err := tx.QueryRow(`SELECT tenant_id FROM deliveryservice where id = $1`, id).Scan(&tenantID); err != nil {
-		if err == sql.ErrNoRows {
-			return nil, false, nil
-		}
-		return nil, false, fmt.Errorf("querying tenant ID for delivery service ID '%v': %v", id, err)
-	}
-	return tenantID, true, nil
-}
-
 // getDSTenantIDByName returns the tenant ID, whether the delivery service exists, and any error.
-func getDSTenantIDByName(tx *sql.Tx, name string) (*int, bool, error) {
-	tenantID := (*int)(nil)
-	if err := tx.QueryRow(`SELECT tenant_id FROM deliveryservice where xml_id = $1`, name).Scan(&tenantID); err != nil {
-		if err == sql.ErrNoRows {
-			return nil, false, nil
-		}
-		return nil, false, fmt.Errorf("querying tenant ID for delivery service name '%v': %v", name, err)
-	}
-	return tenantID, true, nil
-}
-
-// GetDSTenantIDByNameTx returns the tenant ID, whether the delivery service exists, and any error.
-func GetDSTenantIDByNameTx(tx *sql.Tx, ds tc.DeliveryServiceName) (*int, bool, error) {
+func getDSTenantIDByName(tx *sql.Tx, ds tc.DeliveryServiceName) (*int, bool, error) {
 	tenantID := (*int)(nil)
 	if err := tx.QueryRow(`SELECT tenant_id FROM deliveryservice where xml_id = $1`, ds).Scan(&tenantID); err != nil {
 		if err == sql.ErrNoRows {
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
deleted file mode 100644
index 68d9c21..0000000
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv12.go
+++ /dev/null
@@ -1,191 +0,0 @@
-package deliveryservice
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import (
-	"encoding/json"
-	"errors"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/lib/go-util"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
-
-	"github.com/lib/pq"
-)
-
-type TODeliveryServiceV12 struct {
-	api.APIInfoImpl
-	tc.DeliveryServiceNullableV12
-}
-
-func (ds TODeliveryServiceV12) MarshalJSON() ([]byte, error) {
-	return json.Marshal(ds.DeliveryServiceNullableV12)
-}
-
-func (ds *TODeliveryServiceV12) UnmarshalJSON(data []byte) error {
-	return json.Unmarshal(data, ds.DeliveryServiceNullableV12)
-}
-
-func (v *TODeliveryServiceV12) DeleteQuery() string {
-	return `DELETE FROM deliveryservice WHERE id = :id`
-}
-
-func (ds TODeliveryServiceV12) GetKeyFieldsInfo() []api.KeyFieldInfo {
-	return []api.KeyFieldInfo{{"id", api.GetIntKey}}
-}
-
-func (ds TODeliveryServiceV12) GetKeys() (map[string]interface{}, bool) {
-	if ds.ID == nil {
-		return map[string]interface{}{"id": 0}, false
-	}
-	return map[string]interface{}{"id": *ds.ID}, true
-}
-
-func (ds *TODeliveryServiceV12) SetKeys(keys map[string]interface{}) {
-	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
-	ds.ID = &i
-}
-
-func (ds *TODeliveryServiceV12) GetAuditName() string {
-	if ds.XMLID != nil {
-		return *ds.XMLID
-	}
-	return ""
-}
-
-func (ds *TODeliveryServiceV12) GetType() string {
-	return "ds"
-}
-
-// IsTenantAuthorized checks that the user is authorized for both the delivery service's existing tenant, and the new tenant they're changing it to (if different).
-func (ds *TODeliveryServiceV12) IsTenantAuthorized(user *auth.CurrentUser) (bool, error) {
-	tcDS := tc.NewDeliveryServiceNullableFromV12(ds.DeliveryServiceNullableV12)
-	return isTenantAuthorized(ds.ReqInfo, &tcDS)
-}
-
-func (ds *TODeliveryServiceV12) Validate() error {
-	return ds.DeliveryServiceNullableV12.Validate(ds.ReqInfo.Tx.Tx)
-}
-
-func CreateV12(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-	ds := tc.DeliveryServiceNullableV12{}
-	if err := api.Parse(r.Body, inf.Tx.Tx, &ds); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
-		return
-	}
-	tcDS := tc.NewDeliveryServiceNullableFromV12(ds)
-	tcDS, errCode, userErr, sysErr = create(inf, tcDS)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV12{tcDS.DeliveryServiceNullableV12})
-}
-
-func (ds *TODeliveryServiceV12) Read() ([]interface{}, error, error, int) {
-	returnable := []interface{}{}
-	dses, errs, _ := readGetDeliveryServices(ds.APIInfo().Params, ds.APIInfo().Tx, ds.APIInfo().User)
-	if len(errs) > 0 {
-		for _, err := range errs {
-			if err.Error() == `id cannot parse to integer` {
-				return nil, errors.New("Resource not found."), nil, http.StatusNotFound //matches perl response
-			}
-		}
-		return nil, nil, errors.New("reading ds v12: " + util.JoinErrsStr(errs)), http.StatusInternalServerError
-	}
-
-	for _, ds := range dses {
-		returnable = append(returnable, ds.DeliveryServiceNullableV12)
-	}
-	return returnable, nil, nil, http.StatusOK
-}
-
-func UpdateV12(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-
-	ds := tc.DeliveryServiceNullableV12{}
-	ds.ID = util.IntPtr(inf.IntParams["id"])
-	if err := api.Parse(r.Body, inf.Tx.Tx, &ds); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
-		return
-	}
-	tcDS := tc.NewDeliveryServiceNullableFromV12(ds)
-	tcDS, errCode, userErr, sysErr = update(inf, &tcDS)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullableV12{tcDS.DeliveryServiceNullableV12})
-}
-
-//Delete is the DeliveryService implementation of the Deleter interface.
-func (ds *TODeliveryServiceV12) Delete() (error, error, int) {
-	if ds.ID == nil {
-		return errors.New("missing id"), nil, http.StatusBadRequest
-	}
-
-	xmlID, ok, err := GetXMLID(ds.ReqInfo.Tx.Tx, *ds.ID)
-	if err != nil {
-		return nil, errors.New("dsv12 delete: getting xmlid: " + err.Error()), http.StatusInternalServerError
-	} else if !ok {
-		return errors.New("delivery service not found"), nil, http.StatusNotFound
-	}
-	ds.XMLID = &xmlID
-
-	// Note ds regexes MUST be deleted before the ds, because there's a ON DELETE CASCADE on deliveryservice_regex (but not on regex).
-	// Likewise, it MUST happen in a transaction with the later DS delete, so they aren't deleted if the DS delete fails.
-	if _, err := ds.ReqInfo.Tx.Tx.Exec(`DELETE FROM regex WHERE id IN (SELECT regex FROM deliveryservice_regex WHERE deliveryservice=$1)`, *ds.ID); err != nil {
-		return nil, errors.New("TODeliveryServiceV12.Delete deleting regexes for delivery service: " + err.Error()), http.StatusInternalServerError
-	}
-
-	if _, err := ds.ReqInfo.Tx.Tx.Exec(`DELETE FROM deliveryservice_regex WHERE deliveryservice=$1`, *ds.ID); err != nil {
-		return nil, errors.New("TODeliveryServiceV12.Delete deleting delivery service regexes: " + err.Error()), http.StatusInternalServerError
-	}
-
-	userErr, sysErr, errCode := api.GenericDelete(ds)
-	if userErr != nil || sysErr != nil {
-		return userErr, sysErr, errCode
-	}
-
-	paramConfigFilePrefixes := []string{"hdr_rw_", "hdr_rw_mid_", "regex_remap_", "cacheurl_"}
-	configFiles := []string{}
-	for _, prefix := range paramConfigFilePrefixes {
-		configFiles = append(configFiles, prefix+*ds.XMLID+".config")
-	}
-
-	if _, err := ds.ReqInfo.Tx.Tx.Exec(`DELETE FROM parameter WHERE name = 'location' AND config_file = ANY($1)`, pq.Array(configFiles)); err != nil {
-		return nil, errors.New("TODeliveryServiceV12.Delete deleting delivery service parameteres: " + err.Error()), http.StatusInternalServerError
-	}
-
-	return nil, nil, http.StatusOK
-}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
deleted file mode 100644
index 6497b60..0000000
--- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservicesv13.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package deliveryservice
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import (
-	"encoding/json"
-	"errors"
-	"net/http"
-
-	"github.com/apache/trafficcontrol/lib/go-tc"
-	"github.com/apache/trafficcontrol/lib/go-util"
-	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
-)
-
-//we need a type alias to define functions on
-
-type TODeliveryServiceV13 struct {
-	api.APIInfoImpl
-	tc.DeliveryServiceNullableV13
-}
-
-func (ds TODeliveryServiceV13) MarshalJSON() ([]byte, error) {
-	return json.Marshal(ds.DeliveryServiceNullableV13)
-}
-
-func (ds *TODeliveryServiceV13) UnmarshalJSON(data []byte) error {
-	return json.Unmarshal(data, ds.DeliveryServiceNullableV13)
-}
-
-func (ds *TODeliveryServiceV13) APIInfo() *api.APIInfo { return ds.ReqInfo }
-
-func (ds *TODeliveryServiceV13) SetKeys(keys map[string]interface{}) {
-	i, _ := keys["id"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here.
-	ds.ID = &i
-}
-
-func (ds *TODeliveryServiceV13) Validate() error {
-	return ds.DeliveryServiceNullableV13.Validate(ds.APIInfo().Tx.Tx)
-}
-
-// 	TODO allow users to post names (type, cdn, etc) and get the IDs from the names. This isn't trivial to do in a single query, without dynamically building the entire insert query, and ideally inserting would be one query. But it'd be much more convenient for users. Alternatively, remove IDs from the database entirely and use real candidate keys.
-func CreateV13(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-
-	ds := tc.DeliveryServiceNullableV13{}
-	if err := api.Parse(r.Body, inf.Tx.Tx, &ds); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("decoding: "+err.Error()), nil)
-		return
-	}
-
-	if ds.RoutingName == nil || *ds.RoutingName == "" {
-		ds.RoutingName = util.StrPtr("cdn")
-	}
-	if err := ds.Validate(inf.Tx.Tx); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("invalid request: "+err.Error()), nil)
-		return
-	}
-	tcDS := tc.NewDeliveryServiceNullableFromV13(ds)
-	tcDS, errCode, userErr, sysErr = create(inf, tcDS)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice creation was successful.", []tc.DeliveryServiceNullableV13{tcDS.DeliveryServiceNullableV13})
-}
-
-func (ds *TODeliveryServiceV13) Read() ([]interface{}, error, error, int) {
-	returnable := []interface{}{}
-	dses, errs, _ := readGetDeliveryServices(ds.APIInfo().Params, ds.APIInfo().Tx, ds.APIInfo().User)
-	if len(errs) > 0 {
-		for _, err := range errs {
-			if err.Error() == `id cannot parse to integer` { // TODO create const for string
-				return nil, errors.New("Resource not found."), nil, http.StatusNotFound //matches perl response
-			}
-		}
-		return nil, nil, errors.New("reading dses: " + util.JoinErrsStr(errs)), http.StatusInternalServerError
-	}
-
-	for _, ds := range dses {
-		returnable = append(returnable, ds.DeliveryServiceNullableV13)
-	}
-	return returnable, nil, nil, http.StatusOK
-}
-
-func UpdateV13(w http.ResponseWriter, r *http.Request) {
-	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"id"})
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	defer inf.Close()
-
-	id := inf.IntParams["id"]
-
-	ds := tc.DeliveryServiceNullable{}
-	if err := json.NewDecoder(r.Body).Decode(&ds); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
-		return
-	}
-	ds.ID = &id
-
-	if err := ds.Validate(inf.Tx.Tx); err != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("invalid request: "+err.Error()), nil)
-		return
-	}
-
-	ds, errCode, userErr, sysErr = update(inf, &ds)
-	if userErr != nil || sysErr != nil {
-		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
-		return
-	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Deliveryservice update was successful.", []tc.DeliveryServiceNullable{ds})
-}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go b/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go
index dab511a..acd3922 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/eligible.go
@@ -38,7 +38,7 @@ func GetServersEligible(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	dsTenantID, ok, err := GetDSTenantIDByIDTx(inf.Tx.Tx, inf.IntParams["id"])
+	dsTenantID, ok, err := getDSTenantIDByID(inf.Tx.Tx, inf.IntParams["id"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go b/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go
index 26facf9..fcdd1c8 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/urlkey.go
@@ -57,7 +57,7 @@ func GetURLKeysByID(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	dsTenantID, ok, err := GetDSTenantIDByIDTx(inf.Tx.Tx, inf.IntParams["id"])
+	dsTenantID, ok, err := getDSTenantIDByID(inf.Tx.Tx, inf.IntParams["id"])
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
 		return
@@ -101,7 +101,7 @@ func GetURLKeysByName(w http.ResponseWriter, r *http.Request) {
 
 	ds := tc.DeliveryServiceName(inf.Params["name"])
 
-	dsTenantID, ok, err := GetDSTenantIDByNameTx(inf.Tx.Tx, ds)
+	dsTenantID, ok, err := getDSTenantIDByName(inf.Tx.Tx, ds)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
 		return
@@ -146,7 +146,7 @@ func CopyURLKeys(w http.ResponseWriter, r *http.Request) {
 	ds := tc.DeliveryServiceName(inf.Params["name"])
 	copyDS := tc.DeliveryServiceName(inf.Params["copy-name"])
 
-	dsTenantID, ok, err := GetDSTenantIDByNameTx(inf.Tx.Tx, ds)
+	dsTenantID, ok, err := getDSTenantIDByName(inf.Tx.Tx, ds)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
 		return
@@ -164,7 +164,7 @@ func CopyURLKeys(w http.ResponseWriter, r *http.Request) {
 	}
 
 	{
-		copyDSTenantID, ok, err := GetDSTenantIDByNameTx(inf.Tx.Tx, copyDS)
+		copyDSTenantID, ok, err := getDSTenantIDByName(inf.Tx.Tx, copyDS)
 		if err != nil {
 			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
 			return
@@ -214,7 +214,7 @@ func GenerateURLKeys(w http.ResponseWriter, r *http.Request) {
 
 	ds := tc.DeliveryServiceName(inf.Params["name"])
 
-	dsTenantID, ok, err := GetDSTenantIDByNameTx(inf.Tx.Tx, ds)
+	dsTenantID, ok, err := getDSTenantIDByName(inf.Tx.Tx, ds)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("checking tenant: "+err.Error()))
 		return
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index f8fb469..b7e4cc8 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -391,23 +391,18 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
 		{1.1, http.MethodPost, `federations/{id}/deliveryservices?(\.json)?$`, federations.PostDSes, auth.PrivLevelAdmin, Authenticated, nil},
 
 		////DeliveryServices
-		{1.4, http.MethodGet, `deliveryservices/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.3, http.MethodGet, `deliveryservices/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryServiceV13{}), auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.1, http.MethodGet, `deliveryservices/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryServiceV12{}), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryservices/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelReadOnly, Authenticated, nil},
+		{1.1, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelReadOnly, Authenticated, nil},
 
-		{1.4, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.3, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryServiceV13{}), auth.PrivLevelReadOnly, Authenticated, nil},
-		{1.1, http.MethodGet, `deliveryservices/{id}/?(\.json)?$`, api.ReadHandler(&deliveryservice.TODeliveryServiceV12{}), auth.PrivLevelReadOnly, Authenticated, nil},
-
-		{1.4, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.Create, auth.PrivLevelOperations, Authenticated, nil},
+		{1.4, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV14, auth.PrivLevelOperations, Authenticated, nil},
 		{1.3, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV13, auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodPost, `deliveryservices/?(\.json)?$`, deliveryservice.CreateV12, auth.PrivLevelOperations, Authenticated, nil},
 
-		{1.4, http.MethodPut, `deliveryservices/{id}/?(\.json)?$`, deliveryservice.Update, auth.PrivLevelOperations, Authenticated, nil},
+		{1.4, http.MethodPut, `deliveryservices/{id}/?(\.json)?$`, deliveryservice.UpdateV14, auth.PrivLevelOperations, Authenticated, nil},
 		{1.3, http.MethodPut, `deliveryservices/{id}/?(\.json)?$`, deliveryservice.UpdateV13, auth.PrivLevelOperations, Authenticated, nil},
 		{1.1, http.MethodPut, `deliveryservices/{id}/?(\.json)?$`, deliveryservice.UpdateV12, auth.PrivLevelOperations, Authenticated, nil},
 
-		{1.1, http.MethodDelete, `deliveryservices/{id}/?(\.json)?$`, api.DeleteHandler(&deliveryservice.TODeliveryServiceV12{}), auth.PrivLevelOperations, Authenticated, nil},
+		{1.1, http.MethodDelete, `deliveryservices/{id}/?(\.json)?$`, api.DeleteHandler(&deliveryservice.TODeliveryService{}), auth.PrivLevelOperations, Authenticated, nil},
 
 		{1.1, http.MethodGet, `deliveryservices/{id}/servers/eligible/?(\.json)?$`, deliveryservice.GetServersEligible, auth.PrivLevelReadOnly, Authenticated, nil},