diff --git a/lib/src/pipeline.dart b/lib/src/pipeline.dart
new file mode 100644
index 0000000000..d7fe1e4d70
--- /dev/null
+++ b/lib/src/pipeline.dart
@@ -0,0 +1,50 @@
+k// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'client.dart';
+import 'handler.dart';
+import 'middleware.dart';
+
+/// A helper that makes it easy to compose a set of [Middleware] and a
+/// [Client].
+///
+///     var client = const Pipeline()
+///         .addMiddleware(loggingMiddleware)
+///         .addMiddleware(basicAuthMiddleware)
+///         .addClient(new Client());
+class Pipeline {
+  /// The outer pipeline.
+  final Pipeline _parent;
+
+  /// The [Middleware] that is invoked at this stage.
+  final Middleware _middleware;
+
+  const Pipeline()
+      : _parent = null,
+        _middleware = null;
+
+  Pipeline._(this._parent, this._middleware);
+
+  /// Returns a new [Pipeline] with [middleware] added to the existing set of
+  /// [Middleware].
+  ///
+  /// [middleware] will be the last [Middleware] to process a request and
+  /// the first to process a response.
+  Pipeline addMiddleware(Middleware middleware) =>
+      new Pipeline._(this, middleware);
+
+  /// Returns a new [Client] with [client] as the final processor of a
+  /// [Request] if all of the middleware in the pipeline have passed the request
+  /// through.
+  Client addClient(Client client) =>
+      _middleware == null ? client : _parent.addClient(_middleware(client));
+
+  /// Returns a new [Client] with [handler] as the final processor of a
+  /// [Request] if all of the middleware in the pipeline have passed the request
+  /// through.
+  Client addHandler(Handler handler) => addClient(new Client.handler(handler));
+
+  /// Exposes this pipeline of [Middleware] as a single middleware instance.
+  Middleware get middleware => addClient;
+}
diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart
new file mode 100644
index 0000000000..cbde3a62e3
--- /dev/null
+++ b/test/pipeline_test.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import 'package:http/http.dart';
+
+void main() {
+  test('compose middleware with Pipeline', () async {
+    var accessLocation = 0;
+
+    var middlewareA = createMiddleware(requestHandler: (request) async {
+      expect(accessLocation, 0);
+      accessLocation = 1;
+      return request;
+    }, responseHandler: (response) async {
+      expect(accessLocation, 4);
+      accessLocation = 5;
+      return response;
+    });
+
+    var middlewareB = createMiddleware(requestHandler: (request) async {
+      expect(accessLocation, 1);
+      accessLocation = 2;
+      return request;
+    }, responseHandler: (response) async {
+      expect(accessLocation, 3);
+      accessLocation = 4;
+      return response;
+    });
+
+    var client = const Pipeline()
+        .addMiddleware(middlewareA)
+        .addMiddleware(middlewareB)
+        .addClient(new Client.handler((request) async {
+      expect(accessLocation, 2);
+      accessLocation = 3;
+      return new Response(Uri.parse('dart:http'), 200);
+    }));
+
+    var response = await client.get(Uri.parse('dart:http'));
+
+    expect(response, isNotNull);
+    expect(accessLocation, 5);
+  });
+
+  test('Pipeline can be used as middleware', () async {
+    int accessLocation = 0;
+
+    var middlewareA = createMiddleware(requestHandler: (request) async {
+      expect(accessLocation, 0);
+      accessLocation = 1;
+      return request;
+    }, responseHandler: (response) async {
+      expect(accessLocation, 4);
+      accessLocation = 5;
+      return response;
+    });
+
+    var middlewareB = createMiddleware(requestHandler: (request) async {
+      expect(accessLocation, 1);
+      accessLocation = 2;
+      return request;
+    }, responseHandler: (response) async {
+      expect(accessLocation, 3);
+      accessLocation = 4;
+      return response;
+    });
+
+    var innerPipeline =
+        const Pipeline().addMiddleware(middlewareA).addMiddleware(middlewareB);
+
+    var client = const Pipeline()
+        .addMiddleware(innerPipeline.middleware)
+        .addClient(new Client.handler((request) async {
+      expect(accessLocation, 2);
+      accessLocation = 3;
+      return new Response(Uri.parse('dart:http'), 200);
+    }));
+
+    var response = await client.get(Uri.parse('dart:http'));
+
+    expect(response, isNotNull);
+    expect(accessLocation, 5);
+  });
+
+  test('Pipeline calls close on all middleware', () {
+    int accessLocation = 0;
+
+    var middlewareA = createMiddleware(onClose: () {
+      expect(accessLocation, 0);
+      accessLocation = 1;
+    });
+
+    var middlewareB = createMiddleware(onClose: () {
+      expect(accessLocation, 1);
+      accessLocation = 2;
+    });
+
+    var client = const Pipeline()
+        .addMiddleware(middlewareA)
+        .addMiddleware(middlewareB)
+        .addClient(new Client.handler((request) async => null, onClose: () {
+          expect(accessLocation, 2);
+          accessLocation = 3;
+        }));
+
+    client.close();
+    expect(accessLocation, 3);
+  });
+}