diff --git a/Makefile b/Makefile
index ef888f5bfdfe18ecf9851498600dafcc2dde3c3f..6ec99023a1fce8806dbf57cf3fa15066414c64ad 100644
--- a/Makefile
+++ b/Makefile
@@ -56,7 +56,7 @@ container: all
 .PHONY: test-unit
 test-unit:
 	@echo Running tests...
-	go test -v $(GO_PKGS)
+	go test -race -v $(GO_PKGS)
 
 .PHONY: test-flake8
 test-flake8:
diff --git a/cmd/manager/deployments.go b/cmd/manager/deployments.go
index 993dc16780ae12b3f22fd7ddbdcd1b0bd7f70bd5..e71a2418e155df5184f5133ae5be12e4435e125b 100644
--- a/cmd/manager/deployments.go
+++ b/cmd/manager/deployments.go
@@ -18,7 +18,7 @@ package main
 
 import (
 	"encoding/json"
-	"flag"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -32,11 +32,11 @@ import (
 
 	"github.com/ghodss/yaml"
 	"github.com/gorilla/mux"
-
 	"github.com/kubernetes/deployment-manager/cmd/manager/manager"
 	"github.com/kubernetes/deployment-manager/cmd/manager/repository"
 	"github.com/kubernetes/deployment-manager/cmd/manager/repository/persistent"
 	"github.com/kubernetes/deployment-manager/cmd/manager/repository/transient"
+	"github.com/kubernetes/deployment-manager/cmd/manager/router"
 	"github.com/kubernetes/deployment-manager/pkg/common"
 	"github.com/kubernetes/deployment-manager/pkg/registry"
 	"github.com/kubernetes/deployment-manager/pkg/util"
@@ -65,57 +65,84 @@ var deployments = []Route{
 	{"GetCredential", "/credentials/{credential}", "GET", getCredentialHandlerFunc, ""},
 }
 
-var (
-	maxLength         = flag.Int64("maxLength", 1024, "The maximum length (KB) of a template.")
-	expanderName      = flag.String("expander", "expandybird-service", "The DNS name of the expander service.")
-	expanderURL       = flag.String("expanderURL", "", "The URL for the expander service.")
-	deployerName      = flag.String("deployer", "resourcifier-service", "The DNS name of the deployer service.")
-	deployerURL       = flag.String("deployerURL", "", "The URL for the deployer service.")
-	credentialFile    = flag.String("credentialFile", "", "Local file to use for credentials.")
-	credentialSecrets = flag.Bool("credentialSecrets", true, "Use secrets for credentials.")
-	mongoName         = flag.String("mongoName", "mongodb", "The DNS name of the mongodb service.")
-	mongoPort         = flag.String("mongoPort", "27017", "The port of the mongodb service.")
-	mongoAddress      = flag.String("mongoAddress", "mongodb:27017", "The address of the mongodb service.")
-)
-
+// Deprecated. Use Context.Manager instead.
 var backend manager.Manager
 
-func init() {
-	if !flag.Parsed() {
-		flag.Parse()
+// Route defines a routing table entry to be registered with gorilla/mux.
+//
+// Route is deprecated. Use router.Routes instead.
+type Route struct {
+	Name        string
+	Path        string
+	Methods     string
+	HandlerFunc http.HandlerFunc
+	Type        string
+}
+
+func registerRoutes(c *router.Context) router.Routes {
+	re := regexp.MustCompile("{[a-z]+}")
+
+	r := router.NewRoutes()
+	r.Add("GET /healthz", healthz)
+
+	// TODO: Replace these routes with updated ones.
+	for _, d := range deployments {
+		path := fmt.Sprintf("%s %s", d.Methods, re.ReplaceAllString(d.Path, "*"))
+		fmt.Printf("\t%s\n", path)
+		r.Add(path, func(w http.ResponseWriter, r *http.Request, c *router.Context) error {
+			d.HandlerFunc(w, r)
+			return nil
+		})
 	}
 
-	routes = append(routes, deployments...)
+	return r
+}
+
+func healthz(w http.ResponseWriter, r *http.Request, c *router.Context) error {
+	c.Log("manager: healthz checkpoint")
+	// TODO: This should check the availability of the repository, and fail if it
+	// cannot connect.
+	fmt.Fprintln(w, "OK")
+	return nil
+}
+
+func setupDependencies(c *router.Context) error {
 	var credentialProvider common.CredentialProvider
-	if *credentialFile != "" {
-		if *credentialSecrets {
-			panic(fmt.Errorf("Both credentialFile and credentialSecrets are set"))
+	if c.Config.CredentialFile != "" {
+		if c.Config.CredentialSecrets {
+			return errors.New("Both credentialFile and credentialSecrets are set")
 		}
 		var err error
-		credentialProvider, err = registry.NewFilebasedCredentialProvider(*credentialFile)
+		credentialProvider, err = registry.NewFilebasedCredentialProvider(c.Config.CredentialFile)
 		if err != nil {
-			panic(fmt.Errorf("cannot create credential provider %s: %s", *credentialFile, err))
+			return fmt.Errorf("cannot create credential provider %s: %s", c.Config.CredentialFile, err)
 		}
 	} else if *credentialSecrets {
 		credentialProvider = registry.NewSecretsCredentialProvider()
 	} else {
 		credentialProvider = registry.NewInmemCredentialProvider()
 	}
-	backend = newManager(credentialProvider)
+	c.CredentialProvider = credentialProvider
+	c.Manager = newManager(c)
+
+	// FIXME: As soon as we can, we need to get rid of this.
+	backend = c.Manager
+	return nil
 }
 
 const expanderPort = "8080"
 const deployerPort = "8080"
 
-func newManager(cp common.CredentialProvider) manager.Manager {
+func newManager(c *router.Context) manager.Manager {
+	cfg := c.Config
 	service := registry.NewInmemRegistryService()
-	registryProvider := registry.NewDefaultRegistryProvider(cp, service)
+	registryProvider := registry.NewDefaultRegistryProvider(c.CredentialProvider, service)
 	resolver := manager.NewTypeResolver(registryProvider, util.DefaultHTTPClient())
-	expander := manager.NewExpander(getServiceURL(*expanderURL, *expanderName, expanderPort), resolver)
-	deployer := manager.NewDeployer(getServiceURL(*deployerURL, *deployerName, deployerPort))
-	address := strings.TrimPrefix(getServiceURL(*mongoAddress, *mongoName, *mongoPort), "http://")
+	expander := manager.NewExpander(getServiceURL(cfg.ExpanderURL, cfg.ExpanderName, expanderPort), resolver)
+	deployer := manager.NewDeployer(getServiceURL(cfg.DeployerURL, cfg.DeployerName, deployerPort))
+	address := strings.TrimPrefix(getServiceURL(cfg.MongoAddress, cfg.MongoName, cfg.MongoPort), "http://")
 	repository := createRepository(address)
-	return manager.NewManager(expander, deployer, repository, registryProvider, service, cp)
+	return manager.NewManager(expander, deployer, repository, registryProvider, service, c.CredentialProvider)
 }
 
 func createRepository(address string) repository.Repository {
diff --git a/cmd/manager/main.go b/cmd/manager/main.go
index a4c5df85a89f6e1517f2d6af2b6d03ba079f9135..7aca7e80a0a13780b61bb3bde63191156fbe7c14 100644
--- a/cmd/manager/main.go
+++ b/cmd/manager/main.go
@@ -17,75 +17,64 @@ limitations under the License.
 package main
 
 import (
-	"github.com/kubernetes/deployment-manager/pkg/util"
+	"github.com/kubernetes/deployment-manager/cmd/manager/router"
 	"github.com/kubernetes/deployment-manager/pkg/version"
 
 	"flag"
 	"fmt"
-	"log"
 	"net/http"
 	"os"
-
-	"github.com/gorilla/handlers"
-	"github.com/gorilla/mux"
 )
 
-// Route defines a routing table entry to be registered with gorilla/mux.
-type Route struct {
-	Name        string
-	Path        string
-	Methods     string
-	HandlerFunc http.HandlerFunc
-	Type        string
-}
-
-var routes = []Route{
-	{"HealthCheck", "/healthz", "GET", healthCheckHandlerFunc, ""},
-}
-
-// port to listen on
-var port = flag.Int("port", 8080, "The port to listen on")
+var (
+	port              = flag.Int("port", 8080, "The port to listen on")
+	maxLength         = flag.Int64("maxLength", 1024, "The maximum length (KB) of a template.")
+	expanderName      = flag.String("expander", "expandybird-service", "The DNS name of the expander service.")
+	expanderURL       = flag.String("expanderURL", "", "The URL for the expander service.")
+	deployerName      = flag.String("deployer", "resourcifier-service", "The DNS name of the deployer service.")
+	deployerURL       = flag.String("deployerURL", "", "The URL for the deployer service.")
+	credentialFile    = flag.String("credentialFile", "", "Local file to use for credentials.")
+	credentialSecrets = flag.Bool("credentialSecrets", true, "Use secrets for credentials.")
+	mongoName         = flag.String("mongoName", "mongodb", "The DNS name of the mongodb service.")
+	mongoPort         = flag.String("mongoPort", "27017", "The port of the mongodb service.")
+	mongoAddress      = flag.String("mongoAddress", "mongodb:27017", "The address of the mongodb service.")
+)
 
-func init() {
-	if !flag.Parsed() {
-		flag.Parse()
+func main() {
+	// Set up dependencies
+	c := &router.Context{
+		Config: parseFlags(),
 	}
-}
 
-func main() {
-	if !flag.Parsed() {
-		flag.Parse()
+	if err := setupDependencies(c); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
 	}
 
-	router := mux.NewRouter()
-	router.StrictSlash(true)
-	for _, route := range routes {
-		handler := http.Handler(http.HandlerFunc(route.HandlerFunc))
-		switch route.Type {
-		case "JSON":
-			handler = handlers.ContentTypeHandler(handler, "application/json")
-		case "":
-			break
-		default:
-			log.Fatalf("invalid route type: %v", route.Type)
-		}
+	// Set up routes
+	routes := registerRoutes(c)
 
-		r := router.NewRoute()
-		r.Name(route.Name).
-			Path(route.Path).
-			Methods(route.Methods).
-			Handler(handler)
+	// Now create a server.
+	c.Log("Starting Manager %s on %s", version.Version, c.Config.Address)
+	if err := http.ListenAndServe(c.Config.Address, router.NewHandler(c, routes)); err != nil {
+		c.Err("Server exited with error %s", err)
+		os.Exit(1)
 	}
-
-	address := fmt.Sprintf(":%d", *port)
-	handler := handlers.CombinedLoggingHandler(os.Stderr, router)
-	log.Printf("Version: %s", version.Version)
-	log.Printf("Listening on port %d...", *port)
-	log.Fatal(http.ListenAndServe(address, handler))
 }
 
-func healthCheckHandlerFunc(w http.ResponseWriter, r *http.Request) {
-	handler := "manager: get health"
-	util.LogHandlerEntry(handler, r)
-	util.LogHandlerExitWithText(handler, w, "OK", http.StatusOK)
+func parseFlags() *router.Config {
+	flag.Parse()
+	return &router.Config{
+		Address:           fmt.Sprintf(":%d", *port),
+		MaxTemplateLength: *maxLength,
+		ExpanderName:      *expanderName,
+		ExpanderURL:       *expanderURL,
+		DeployerName:      *deployerName,
+		DeployerURL:       *deployerURL,
+		CredentialFile:    *credentialFile,
+		CredentialSecrets: *credentialSecrets,
+		MongoName:         *mongoName,
+		MongoPort:         *mongoPort,
+		MongoAddress:      *mongoAddress,
+	}
 }
diff --git a/cmd/manager/router/encoder.go b/cmd/manager/router/encoder.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd401cb59c905296d310b63bbbb9764a0f0ee44e
--- /dev/null
+++ b/cmd/manager/router/encoder.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+
+Licensed 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.
+*/
+
+package router
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"mime"
+	"net/http"
+	"reflect"
+	"strings"
+
+	"github.com/ghodss/yaml"
+)
+
+type Encoder interface {
+	// Encode encoders a given response
+	//
+	// When an encoder fails, it logs any necessary data and then responds to
+	// the client.
+	Encode(http.ResponseWriter, *http.Request, *Context, interface{})
+}
+
+// AcceptEncodder uses the accept headers on a request to determine the response type.
+//
+// It supports the following encodings:
+//	- application/json: passed to encoding/json.Marshal
+//	- text/yaml: passed to gopkg.in/yaml.v2.Marshal
+//	- text/plain: passed to fmt.Sprintf("%V")
+type AcceptEncoder struct {
+	DefaultEncoding string
+}
+
+// Encode encodeds the given interface to the first available type in the Accept header.
+func (e *AcceptEncoder) Encode(w http.ResponseWriter, r *http.Request, c *Context, out interface{}) {
+	a := r.Header.Get("accept")
+	fn := encoders[e.DefaultEncoding]
+	mt := e.DefaultEncoding
+	if a != "" {
+		mt, fn = e.parseAccept(a)
+	}
+
+	data, err := fn(out)
+	if err != nil {
+		Fatal(w, r, "Could not marshal data: %s", err)
+		return
+	}
+	w.Header().Add("content-type", mt)
+	w.Write(data)
+}
+
+// parseAccept parses the value of an Accept: header and returns the best match.
+//
+// This returns the matched MIME type and the Marshal function.
+func (e *AcceptEncoder) parseAccept(h string) (string, Marshaler) {
+
+	keys := strings.Split(h, ",")
+	for _, k := range keys {
+		mt, _, err := mime.ParseMediaType(k)
+		if err != nil {
+			continue
+		}
+		if enc, ok := encoders[mt]; ok {
+			return mt, enc
+		}
+	}
+	return e.DefaultEncoding, encoders[e.DefaultEncoding]
+}
+
+type Marshaler func(interface{}) ([]byte, error)
+
+var encoders = map[string]Marshaler{
+	"application/json":   json.Marshal,
+	"text/yaml":          yaml.Marshal,
+	"application/x-yaml": yaml.Marshal,
+	"text/plain":         textMarshal,
+}
+
+var ErrUnsupportedKind = errors.New("unsupported kind")
+
+// textMarshal marshals v into a text representation ONLY IN NARROW CASES.
+//
+//	An error will have its Error() method called.
+//	A fmt.Stringer will have its String() method called.
+//	Scalar types will be marshaled with fmt.Sprintf("%v").
+//
+// This will only marshal scalar types for securoty reasons (namely, we don't
+// want the possibility of forcing exposure of non-exported data or ptr
+// addresses, etc.)
+func textMarshal(v interface{}) ([]byte, error) {
+	switch s := v.(type) {
+	case error:
+		return []byte(s.Error()), nil
+	case fmt.Stringer:
+		return []byte(s.String()), nil
+	}
+
+	// Error on kinds we don't support.
+	val := reflect.Indirect(reflect.ValueOf(v))
+	switch val.Kind() {
+	case reflect.Invalid, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface,
+		reflect.Map, reflect.Ptr, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
+		return []byte{}, ErrUnsupportedKind
+	}
+	return []byte(fmt.Sprintf("%v", v)), nil
+}
diff --git a/cmd/manager/router/encoder_test.go b/cmd/manager/router/encoder_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..283581b3e03da69a424ee521fa05213907f43162
--- /dev/null
+++ b/cmd/manager/router/encoder_test.go
@@ -0,0 +1,110 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+
+Licensed 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.
+*/
+
+package router
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"testing"
+)
+
+var _ Encoder = &AcceptEncoder{}
+
+func TestParseAccept(t *testing.T) {
+	e := &AcceptEncoder{
+		DefaultEncoding: "application/json",
+	}
+	tests := map[string]string{
+		"":    e.DefaultEncoding,
+		"*/*": e.DefaultEncoding,
+		// To stay true to spec, this _should_ be an error. But our thought
+		// on this case is that we'd rather send a default format.
+		"audio/*; q=0.2, audio/basic":                  e.DefaultEncoding,
+		"text/html; q=0.8, text/yaml,application/json": "text/yaml",
+		"application/x-yaml; foo=bar":                  "application/x-yaml",
+		"text/monkey,     TEXT/YAML ; zoom=zoom   ":    "text/yaml",
+	}
+
+	for in, expects := range tests {
+		mt, enc := e.parseAccept(in)
+		if mt != expects {
+			t.Errorf("Expected %q, got %q", expects, mt)
+			continue
+		}
+		_, err := enc([]string{"hello", "world"})
+		if err != nil {
+			t.Fatalf("Failed to marshal: %s", err)
+		}
+	}
+}
+
+func TestTextMarshal(t *testing.T) {
+	tests := map[string]interface{}{
+		"foo":           "foo",
+		"5":             5,
+		"stinky cheese": errors.New("stinky cheese"),
+	}
+	for expect, in := range tests {
+		if o, err := textMarshal(in); err != nil || string(o) != expect {
+			t.Errorf("Expected %q, got %q", expect, o)
+		}
+	}
+
+	if _, err := textMarshal(struct{ foo int }{5}); err != ErrUnsupportedKind {
+		t.Fatalf("Expected unsupported kind, got %v", err)
+	}
+}
+
+func TestAcceptEncoder(t *testing.T) {
+	c := &Context{
+		Encoder: &AcceptEncoder{DefaultEncoding: "application/json"},
+	}
+	fn := func(w http.ResponseWriter, r *http.Request, c *Context) error {
+		c.Encoder.Encode(w, r, c, []string{"hello", "world"})
+		return nil
+	}
+	s := httpHarness(c, "GET /", fn)
+	defer s.Close()
+
+	res, err := http.Get(s.URL)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res.StatusCode != 200 {
+		t.Fatalf("Unexpected response code %d", res.StatusCode)
+	}
+	if mt := res.Header.Get("content-type"); mt != "application/json" {
+		t.Errorf("Unexpected content type: %q", mt)
+	}
+
+	data, err := ioutil.ReadAll(res.Body)
+	res.Body.Close()
+	if err != nil {
+		t.Fatalf("Failed to read response body: %s", err)
+	}
+
+	out := []string{}
+	if err := json.Unmarshal(data, &out); err != nil {
+		t.Fatalf("Failed to unmarshal JSON: %s", err)
+	}
+
+	if out[0] != "hello" {
+		t.Fatalf("Unexpected JSON data in slot 0: %s", out[0])
+	}
+}
diff --git a/cmd/manager/router/router.go b/cmd/manager/router/router.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f15292cd9d1fdcd38ef8d7a063936f1f614c469
--- /dev/null
+++ b/cmd/manager/router/router.go
@@ -0,0 +1,203 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+
+Licensed 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.
+*/
+
+/* Package router is an HTTP router.
+
+This router provides appropriate dependency injection/encapsulation for the
+HTTP routing layer. This removes the requirement to set global variables for
+resources like database handles.
+
+This library does not replace the default HTTP mux because there is no need.
+Instead, it implements an HTTP handler.
+
+It then defines a handler function that is given a context as well as a
+request and response.
+*/
+package router
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+
+	"github.com/Masterminds/httputil"
+	"github.com/kubernetes/deployment-manager/cmd/manager/manager"
+	"github.com/kubernetes/deployment-manager/pkg/common"
+)
+
+const LogAccess = "Access: %s %s"
+
+// Config holds the global configuration parameters passed into the router.
+//
+// Config is used concurrently. Once a config is created, it should be treated
+// as immutable.
+type Config struct {
+	// Address is the host and port (:8080)
+	Address string
+	// MaxTemplateLength is the maximum length of a template.
+	MaxTemplateLength int64
+	// ExpanderName is the DNS name of the expansion service.
+	ExpanderName string
+	// ExpanderURL is the expander service's URL.
+	ExpanderURL string
+	// DeployerName is the deployer's DNS name
+	DeployerName string
+	// DeployerURL is the deployer's URL
+	DeployerURL string
+	// CredentialFile is the file to the credentials.
+	CredentialFile string
+	// CredentialSecrets tells the service to use a secrets file instead.
+	CredentialSecrets bool
+	// MongoName is the DNS name of the mongo server.
+	MongoName string
+	// MongoPort is the port for the MongoDB protocol on the mongo server.
+	// It is a string for historical reasons.
+	MongoPort string
+	// MongoAddress is the name and port.
+	MongoAddress string
+}
+
+// Context contains dependencies that are passed to each handler function.
+//
+// Context carries typed information, often scoped to interfaces, so that the
+// caller's contract with the service is known at compile time.
+//
+// Members of the context must be concurrency safe.
+type Context struct {
+	Config *Config
+	// Manager is a deployment-manager/manager/manager.Manager
+	Manager            manager.Manager
+	Encoder            Encoder
+	CredentialProvider common.CredentialProvider
+}
+
+func (c *Context) Log(msg string, v ...interface{}) {
+	// FIXME: This should be configurable via the context.
+	fmt.Fprintf(os.Stdout, msg+"\n", v...)
+}
+
+func (c *Context) Err(msg string, v ...interface{}) {
+	// FIXME: This should be configurable via the context.
+	fmt.Fprintf(os.Stderr, msg+"\n", v...)
+}
+
+// NotFound writes a 404 error to the client and logs an error.
+func NotFound(w http.ResponseWriter, r *http.Request) {
+	// TODO: Log this.
+	w.WriteHeader(http.StatusNotFound)
+	fmt.Fprintln(w, "File Not Found")
+}
+
+// Fatal writes a 500 response to the client and logs the message.
+//
+// Additional arguments are past into the the formatter as params to msg.
+func Fatal(w http.ResponseWriter, r *http.Request, msg string, v ...interface{}) {
+	// TODO: Log this.
+	w.WriteHeader(http.StatusInternalServerError)
+	fmt.Fprintln(w, "Internal Server Error")
+}
+
+// HandlerFunc responds to an individual HTTP request.
+//
+// Returned errors will be captured, logged, and returned as HTTP 500 errors.
+type HandlerFunc func(w http.ResponseWriter, r *http.Request, c *Context) error
+
+// Handler implements an http.Handler.
+//
+// This is the top level route handler.
+type Handler struct {
+	c        *Context
+	resolver *httputil.Resolver
+	routes   Routes
+}
+
+// Create a new Handler.
+//
+// Routes cannot be modified after construction. The order that the route
+// names are returned by Routes.Paths() determines the lookup order.
+func NewHandler(c *Context, r Routes) *Handler {
+	paths := make([]string, r.Len())
+	i := 0
+	for _, k := range r.Paths() {
+		paths[i] = k
+		i++
+	}
+
+	return &Handler{
+		c:        c,
+		resolver: httputil.NewResolver(paths),
+		routes:   r,
+	}
+}
+
+// ServeHTTP serves an HTTP request.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	h.c.Log(LogAccess, r.Method, r.URL)
+	route, err := h.resolver.Resolve(r)
+	if err != nil {
+		NotFound(w, r)
+		return
+	}
+
+	fn, ok := h.routes.Get(route)
+	if !ok {
+		Fatal(w, r, "route %s missing", route)
+	}
+
+	if err := fn(w, r, h.c); err != nil {
+		Fatal(w, r, err.Error())
+	}
+}
+
+// Routes defines a container for route-to-function mapping.
+type Routes interface {
+	Add(string, HandlerFunc)
+	Get(string) (HandlerFunc, bool)
+	Len() int
+	Paths() []string
+}
+
+// NewRoutes creates a default implementation of a Routes.
+//
+// The ordering of routes is nonderministic.
+func NewRoutes() Routes {
+	return routeMap{}
+}
+
+type routeMap map[string]HandlerFunc
+
+func (r routeMap) Add(name string, fn HandlerFunc) {
+	r[name] = fn
+}
+
+func (r routeMap) Get(name string) (HandlerFunc, bool) {
+	f, ok := r[name]
+	return f, ok
+}
+
+func (r routeMap) Len() int {
+	return len(r)
+}
+
+func (r routeMap) Paths() []string {
+	b := make([]string, len(r))
+	i := 0
+	for k := range r {
+		b[i] = k
+		i++
+	}
+	return b
+}
diff --git a/cmd/manager/router/router_test.go b/cmd/manager/router/router_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..df673ad04a26b662073ae9a33bb299e2894bbbb9
--- /dev/null
+++ b/cmd/manager/router/router_test.go
@@ -0,0 +1,72 @@
+/*
+Copyright 2016 The Kubernetes Authors All rights reserved.
+
+Licensed 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.
+*/
+
+package router
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+// Canary
+var v Routes = routeMap{}
+
+func TestHandler(t *testing.T) {
+	c := &Context{}
+	r := NewRoutes()
+
+	r.Add("GET /", func(w http.ResponseWriter, r *http.Request, c *Context) error {
+		fmt.Fprintln(w, "hello")
+		return nil
+	})
+	r.Add("POST /", func(w http.ResponseWriter, r *http.Request, c *Context) error {
+		fmt.Fprintln(w, "goodbye")
+		return nil
+	})
+
+	h := NewHandler(c, r)
+
+	s := httptest.NewServer(h)
+	defer s.Close()
+
+	res, err := http.Get(s.URL)
+	if err != nil {
+		t.Fatal(err)
+	}
+	data, err := ioutil.ReadAll(res.Body)
+	res.Body.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if "hello\n" != string(data) {
+		t.Errorf("Expected 'hello', got %q", data)
+	}
+}
+
+// httpHarness is a simple test server fixture.
+// Simple fixture for standing up a test server with a single route.
+//
+// You must Close() the returned server.
+func httpHarness(c *Context, route string, fn HandlerFunc) *httptest.Server {
+	r := NewRoutes()
+	r.Add(route, fn)
+	h := NewHandler(c, r)
+	return httptest.NewServer(h)
+}
diff --git a/glide.lock b/glide.lock
index 2a683ff75816ce69da337e20b6e87c8480a20316..67961e9058460d74e2c84d49e5ac8a139bfcfadb 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,5 +1,5 @@
-hash: d9beab9a799ac8dd0d76c4f7a3a32753d44833dd3527b3caa8e786865ea26816
-updated: 2016-03-04T09:54:13.155442463-07:00
+hash: f34e830b237fcba5202f84c55b21764226b7e8c201f7622e9639c7a628e33b0b
+updated: 2016-03-10T15:03:34.650809572-07:00
 imports:
 - name: github.com/aokoli/goutils
   version: 9c37978a95bd5c709a15883b6242714ea6709e64
@@ -25,10 +25,12 @@ imports:
   version: 8f2758070a82adb7a3ad6b223a0b91878f32d400
 - name: github.com/gorilla/mux
   version: 26a6070f849969ba72b72256e9f14cf519751690
+- name: github.com/Masterminds/httputil
+  version: e9b977e9cf16f9d339573e18f0f1f7ce5d3f419a
 - name: github.com/Masterminds/semver
   version: c4f7ef0702f269161a60489ccbbc9f1241ad1265
 - name: github.com/Masterminds/sprig
-  version: fd057ca403105755181f84645696d705a58852dd
+  version: 0199893f008a87287bf2b4e3e390e66bb074c659
 - name: golang.org/x/net
   version: 04b9de9b512f58addf28c9853d50ebef61c3953e
   subpackages:
diff --git a/glide.yaml b/glide.yaml
index 0ef3eafea1435f21f1a49f9f69aae8b4afb716b8..79237d249d91b39cb715c64f88b33bd66d130d52 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -14,3 +14,4 @@ import:
 - package: github.com/gorilla/mux
 - package: gopkg.in/yaml.v2
 - package: github.com/Masterminds/sprig
+- package: github.com/Masterminds/httputil