diff --git a/dist.ini b/dist.ini
index bdb47fa1be..52f364a5ea 100644
--- a/dist.ini
+++ b/dist.ini
@@ -8,6 +8,7 @@ version = 0.0.1
 [Prereqs]
 AnyEvent::HTTP = 0
 Catalyst::Engine::PSGI = 0
+Catalyst::Model::Adaptor = 0
 Catalyst::Plugin::Authentication = 0
 Catalyst::Plugin::ConfigLoader = 0
 Catalyst::Plugin::Static::Simple = 0
diff --git a/lib/MetaCPAN/Web/API.pm b/lib/MetaCPAN/Web/API.pm
new file mode 100644
index 0000000000..1147c78544
--- /dev/null
+++ b/lib/MetaCPAN/Web/API.pm
@@ -0,0 +1,97 @@
+package MetaCPAN::Web::API;
+
+use Moose;
+use Class::MOP;
+use namespace::autoclean;
+with 'MetaCPAN::Web::API::Request';
+
+has author => (
+    builder    => '_build_author',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::Author',
+    lazy_build => 1,
+);
+
+has favorite => (
+    builder    => '_build_favorite',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::Favorite',
+    lazy_build => 1,
+);
+
+has mirror => (
+    builder    => '_build_mirror',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::Mirror',
+    lazy_build => 1,
+);
+
+has module => (
+    builder    => '_build_module',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::Module',
+    lazy_build => 1,
+);
+
+has rating => (
+    builder    => '_build_rating',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::Rating',
+    lazy_build => 1,
+);
+
+has release => (
+    builder    => '_build_release',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::Release',
+    lazy_build => 1,
+);
+
+has user => (
+    builder    => '_build_user',
+    is         => 'ro',
+    isa        => 'MetaCPAN::Web::API::User',
+    lazy_build => 1,
+);
+
+sub _build_author {
+    shift->_build_instance_of('MetaCPAN::Web::API::Author');
+}
+
+sub _build_favorite {
+    shift->_build_instance_of('MetaCPAN::Web::API::Favorite');
+}
+
+sub _build_instance_of {
+    my ( $self, $class ) = @_;
+    Class::MOP::load_class($class);
+    return $class->new(
+        url        => $self->url,
+        url_secure => $self->url_secure,
+        api        => $self,
+    );
+}
+
+sub _build_mirror {
+    shift->_build_instance_of('MetaCPAN::Web::API::Mirror');
+}
+
+sub _build_module {
+    shift->_build_instance_of('MetaCPAN::Web::API::Module');
+}
+
+sub _build_rating {
+    shift->_build_instance_of('MetaCPAN::Web::API::Rating');
+}
+
+sub _build_release {
+    shift->_build_instance_of('MetaCPAN::Web::API::Release');
+}
+
+sub _build_user {
+    shift->_build_instance_of('MetaCPAN::Web::API::User');
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/MetaCPAN/Web/API/Author.pm b/lib/MetaCPAN/Web/API/Author.pm
new file mode 100644
index 0000000000..27019fd2b9
--- /dev/null
+++ b/lib/MetaCPAN/Web/API/Author.pm
@@ -0,0 +1,20 @@
+package MetaCPAN::Web::API::Author;
+
+use Moose;
+use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
+
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
+
+sub get {
+    my ( $self, $author ) = @_;
+    $self->request( "/author/" . uc($author) );
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/MetaCPAN/Web/Model/API/Favorite.pm b/lib/MetaCPAN/Web/API/Favorite.pm
similarity index 91%
rename from lib/MetaCPAN/Web/Model/API/Favorite.pm
rename to lib/MetaCPAN/Web/API/Favorite.pm
index baaac94011..17756b343c 100644
--- a/lib/MetaCPAN/Web/Model/API/Favorite.pm
+++ b/lib/MetaCPAN/Web/API/Favorite.pm
@@ -1,10 +1,15 @@
-package MetaCPAN::Web::Model::API::Favorite;
+package MetaCPAN::Web::API::Favorite;
+
 use Moose;
+use List::MoreUtils qw(uniq);
 use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
 
-extends 'MetaCPAN::Web::Model::API';
-
-use List::MoreUtils qw(uniq);
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
 
 sub get {
     my ( $self, $user, @distributions ) = @_;
@@ -64,3 +69,5 @@ sub get {
 }
 
 __PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/MetaCPAN/Web/API/Mirror.pm b/lib/MetaCPAN/Web/API/Mirror.pm
new file mode 100644
index 0000000000..67bb3e6f51
--- /dev/null
+++ b/lib/MetaCPAN/Web/API/Mirror.pm
@@ -0,0 +1,41 @@
+package MetaCPAN::Web::API::Mirror;
+
+use Moose;
+use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
+
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
+
+sub list {
+    my ( $self, $location, $protocols ) = @_;
+
+    my @or;
+    push( @or, { not => { filter => { missing => { field => $_ } } } } )
+        for ( @{ $protocols || [] } );
+
+    return $self->request(
+        '/mirror/_search',
+        {   size  => 999,
+            query => { match_all => {} },
+            @or ? ( filter => { and => \@or } ) : (),
+            $location
+            ? ( sort => {
+                    _geo_distance => {
+                        location => [ $location->[1], $location->[0] ],
+                        order    => "asc",
+                        unit     => "km"
+                    }
+                }
+                )
+            : ( sort => [ 'continent', 'country' ] )
+        }
+    );
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/MetaCPAN/Web/Model/API/Module.pm b/lib/MetaCPAN/Web/API/Module.pm
similarity index 95%
rename from lib/MetaCPAN/Web/Model/API/Module.pm
rename to lib/MetaCPAN/Web/API/Module.pm
index 94a5187132..7de62cdb29 100644
--- a/lib/MetaCPAN/Web/Model/API/Module.pm
+++ b/lib/MetaCPAN/Web/API/Module.pm
@@ -1,32 +1,17 @@
-package MetaCPAN::Web::Model::API::Module;
-use Moose;
-use namespace::autoclean;
-
-extends 'MetaCPAN::Web::Model::API';
-
-=head1 NAME
-
-MetaCPAN::Web::Model::Module - Catalyst Model
-
-=head1 DESCRIPTION
-
-Catalyst Model.
-
-=head1 AUTHOR
-
-Moritz Onken, Matthew Phillips
-
-=head1 LICENSE
-
-This library is free software. You can redistribute it and/or modify
-it under the same terms as Perl itself.
-
-=cut
+package MetaCPAN::Web::API::Module;
 
+use Moose;
 use Hash::Merge qw( merge );
-
-use List::Util qw( max sum );
 use List::MoreUtils qw(uniq);
+use List::Util qw( max sum );
+use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
+
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
 
 my $RESULTS_PER_RUN = 200;
 my @ROGUE_DISTRIBUTIONS
@@ -134,8 +119,8 @@ sub search_distribution {
 
             my @ids = map { $_->{fields}->{id} } @{ $data->{hits}->{hits} };
             my $descriptions = $self->search_descriptions(@ids);
-            my $ratings      = $self->model('Rating')->get(@distributions);
-            my $favorites    = $self->model('Favorite')->get($user, @distributions);
+            my $ratings      = $self->api->rating->get(@distributions);
+            my $favorites    = $self->api->favorite->get($user, @distributions);
             return $ratings & $favorites & $descriptions;
         }
         )->(
@@ -190,11 +175,9 @@ sub search_collapsed {
         }
 
         @distributions = splice( @distributions, $from, 20 );
-        my $ratings   = $self->model('Rating')->get(@distributions);
-        my $favorites = $self->model('Favorite')->get($user, @distributions);
-        my $results
-            = $self->model('Module')
-            ->search( $query,
+        my $ratings   = $self->api->rating->get(@distributions);
+        my $favorites = $self->api->favorite->get($user, @distributions);
+        my $results = $self->api->module->search( $query,
             $self->_search_in_distributions(@distributions) );
         return ( $ratings & $favorites & $results );
     };
@@ -461,6 +444,7 @@ sub _search_in_distributions {
             }
         } };
 }
+
 __PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/lib/MetaCPAN/Web/Model/API/Rating.pm b/lib/MetaCPAN/Web/API/Rating.pm
similarity index 79%
rename from lib/MetaCPAN/Web/Model/API/Rating.pm
rename to lib/MetaCPAN/Web/API/Rating.pm
index 88ebbd568b..1786590a3a 100644
--- a/lib/MetaCPAN/Web/Model/API/Rating.pm
+++ b/lib/MetaCPAN/Web/API/Rating.pm
@@ -1,29 +1,15 @@
-package MetaCPAN::Web::Model::API::Rating;
+package MetaCPAN::Web::API::Rating;
+
 use Moose;
+use List::MoreUtils qw(uniq);
 use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
 
-extends 'MetaCPAN::Web::Model::API';
-
-=head1 NAME
-
-MetaCPAN::Web::Model::Rating - Catalyst Model
-
-=head1 DESCRIPTION
-
-Catalyst Model.
-
-=head1 AUTHOR
-
-Moritz Onken, Matthew Phillips
-
-=head1 LICENSE
-
-This library is free software. You can redistribute it and/or modify
-it under the same terms as Perl itself.
-
-=cut
-
-use List::MoreUtils qw(uniq);
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
 
 sub get {
     my ( $self, @distributions ) = @_;
@@ -68,6 +54,7 @@ sub get {
         );
     return $cv;
 }
+
 __PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/lib/MetaCPAN/Web/Model/API/Release.pm b/lib/MetaCPAN/Web/API/Release.pm
similarity index 85%
rename from lib/MetaCPAN/Web/Model/API/Release.pm
rename to lib/MetaCPAN/Web/API/Release.pm
index 800b9f4b19..288d36b8c6 100644
--- a/lib/MetaCPAN/Web/Model/API/Release.pm
+++ b/lib/MetaCPAN/Web/API/Release.pm
@@ -1,27 +1,14 @@
-package MetaCPAN::Web::Model::API::Release;
+package MetaCPAN::Web::API::Release;
+
 use Moose;
 use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
 
-extends 'MetaCPAN::Web::Model::API';
-
-=head1 NAME
-
-MetaCPAN::Web::Model::Release - Catalyst Model
-
-=head1 DESCRIPTION
-
-Catalyst Model.
-
-=head1 AUTHOR
-
-Moritz Onken, Matthew Phillips
-
-=head1 LICENSE
-
-This library is free software. You can redistribute it and/or modify
-it under the same terms as Perl itself.
-
-=cut
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
 
 sub get {
     my ( $self, $author, $release ) = @_;
@@ -42,6 +29,30 @@ sub get {
     );
 }
 
+sub latest_by_author {
+    my ( $self, $author ) = @_;
+    return $self->request(
+        '/release/_search',
+        {   query => {
+                filtered => {
+                    query  => { match_all => {} },
+                    filter => {
+                        and => [
+                            { term => { author => uc($author) } },
+                            { term => { status => 'latest' } }
+                        ]
+                    },
+                }
+            },
+            sort => [
+                'distribution', { 'version_numified' => { reverse => \1 } }
+            ],
+            fields => [qw(author distribution name status abstract date)],
+            size   => 1000,
+        }
+    );
+}
+
 sub recent {
     my ( $self, $page ) = @_;
     $self->request(
diff --git a/lib/MetaCPAN/Web/API/Request.pm b/lib/MetaCPAN/Web/API/Request.pm
new file mode 100644
index 0000000000..6ef5a8f40d
--- /dev/null
+++ b/lib/MetaCPAN/Web/API/Request.pm
@@ -0,0 +1,56 @@
+package MetaCPAN::Web::API::Request;
+
+use Moose::Role;
+use namespace::autoclean;
+
+use AnyEvent::HTTP qw(http_request);
+use JSON;
+use MetaCPAN::Web::MyCondVar;
+use MetaCPAN::Web::API::Result;
+
+sub cv {
+    MetaCPAN::Web::MyCondVar->new;
+}
+
+has url => (
+    default => 'http://api.metacpan.org',
+    is      => 'ro',
+    isa     => 'Str',
+);
+
+has url_secure => (
+    default => 'https://api.metacpan.org',
+    is      => 'ro',
+    isa     => 'Str',
+);
+
+sub request {
+    my ( $self, $path, $search, $params ) = @_;
+    my ( $token, $method ) = @$params{qw(token method)};
+    $path .= "?access_token=$token" if ($token);
+    my $req = $self->cv;
+    http_request $method ? $method
+        : $search        ? 'post'
+        : 'get' => ( $token ? $self->url_secure : $self->url ) . $path,
+        body => $search ? encode_json($search) : undef,
+        headers    => { 'Content-type' => 'application/json' },
+        persistent => 1,
+        sub {
+        my ( $data, $headers ) = @_;
+        my $content_type = $headers->{'content-type'} || '';
+
+        if ( $content_type =~ /^application\/json/ ) {
+            my $json = eval { decode_json($data) };
+            my $result = $@ ? { raw => $data } : $json;
+            $req->send( bless $result, 'MetaCPAN::Web::API::Result' );
+        }
+        else {
+
+            # Response is raw data, e.g. text/plain
+            $req->send( { raw => $data } );
+        }
+        };
+    return $req;
+}
+
+1;
diff --git a/lib/MetaCPAN/Web/API/Result.pm b/lib/MetaCPAN/Web/API/Result.pm
new file mode 100644
index 0000000000..f1c5b3ebf2
--- /dev/null
+++ b/lib/MetaCPAN/Web/API/Result.pm
@@ -0,0 +1,31 @@
+package MetaCPAN::Web::API::Result;
+
+use strict;
+use warnings;
+
+sub fields {
+    my ($self) = @_;
+    return [ map { $_->{fields} } @{ $self->hits } ];
+}
+
+sub hits {
+    my ($self) = @_;
+    return $self->{hits}{hits} || [];
+}
+
+sub source {
+    my ($self) = @_;
+    return [ map { $_->{_source} } @{ $self->hits } ];
+}
+
+sub took {
+    my ($self) = @_;
+    return $self->{took};
+}
+
+sub total {
+    my ($self) = @_;
+    return $self->{hits}{total};
+}
+
+1;
diff --git a/lib/MetaCPAN/Web/Model/API/User.pm b/lib/MetaCPAN/Web/API/User.pm
similarity index 81%
rename from lib/MetaCPAN/Web/Model/API/User.pm
rename to lib/MetaCPAN/Web/API/User.pm
index e0e6226b49..f30bb94e56 100644
--- a/lib/MetaCPAN/Web/Model/API/User.pm
+++ b/lib/MetaCPAN/Web/API/User.pm
@@ -1,8 +1,14 @@
-package MetaCPAN::Web::Model::API::User;
+package MetaCPAN::Web::API::User;
+
 use Moose;
 use namespace::autoclean;
+with qw(MetaCPAN::Web::API::Request);
 
-extends 'MetaCPAN::Web::Model::API';
+has api => (
+    is       => 'ro',
+    isa      => 'MetaCPAN::Web::API',
+    weak_ref => 1,
+);
 
 sub get {
     my ( $self, $token ) = @_;
@@ -37,4 +43,6 @@ sub remove_favorite {
         undef, { method => 'DELETE', token => $token } );
 }
 
+__PACKAGE__->meta->make_immutable;
+
 1;
diff --git a/lib/MetaCPAN/Web/Controller/Account.pm b/lib/MetaCPAN/Web/Controller/Account.pm
index 9f73a3dcab..3aca4694e0 100644
--- a/lib/MetaCPAN/Web/Controller/Account.pm
+++ b/lib/MetaCPAN/Web/Controller/Account.pm
@@ -28,14 +28,14 @@ sub settings : Local {
 sub identities : Local {
     my ( $self, $c ) = @_;
     if ( my $delete = $c->req->params->{delete} ) {
-        $c->model('API::User')->delete_identity( $delete, $c->token )->recv;
+        $c->model('API')->user->delete_identity( $delete, $c->token )->recv;
         $c->res->redirect('/account/identities');
     }
 }
 
 sub profile : Local {
     my ( $self, $c ) = @_;
-    my $author = $c->model('API::User')->get_profile( $c->token )->recv;
+    my $author = $c->model('API')->user->get_profile( $c->token )->recv;
     $c->stash( $author->{error} ? { no_profile => 1 } : { author => $author } );
     my $req = $c->req;
     return unless ( $req->method eq 'POST' );
@@ -69,7 +69,7 @@ sub profile : Local {
     $data->{donation} = undef unless ( $req->params->{donations} );
 
     my $res
-        = $c->model('API::User')->update_profile( $data, $c->token )->recv;
+        = $c->model('API')->user->update_profile( $data, $c->token )->recv;
     if ( $res->{error} ) {
         $c->stash( { author => $data, errors => $res->{errors} } );
     }
diff --git a/lib/MetaCPAN/Web/Controller/Account/Favorite.pm b/lib/MetaCPAN/Web/Controller/Account/Favorite.pm
index f98c89a0ad..995a25f5a0 100644
--- a/lib/MetaCPAN/Web/Controller/Account/Favorite.pm
+++ b/lib/MetaCPAN/Web/Controller/Account/Favorite.pm
@@ -6,7 +6,7 @@ BEGIN { extends 'MetaCPAN::Web::Controller' }
 sub add : Local {
     my ( $self, $c ) = @_;
     $c->detach('/forbidden') unless ( $c->req->method eq 'POST' );
-    my $model = $c->model('API::User');
+    my $model = $c->model('API')->user;
     my $data  = $c->req->params;
     if ( $data->{remove} ) {
         $model->remove_favorite( $data, $c->token )->recv;
diff --git a/lib/MetaCPAN/Web/Controller/Author.pm b/lib/MetaCPAN/Web/Controller/Author.pm
index 462272bd11..b5a12c2dda 100644
--- a/lib/MetaCPAN/Web/Controller/Author.pm
+++ b/lib/MetaCPAN/Web/Controller/Author.pm
@@ -7,38 +7,17 @@ BEGIN { extends 'MetaCPAN::Web::Controller' }
 
 sub index : Path : Args(1) {
     my ( $self, $c, $id ) = @_;
-    my $author_cv = $c->model('API::Author')->get($id);
-    # this should probably be refactored into the model?? why is it here
-    my $releases_cv = $c->model('API::Release')->request(
-        '/release/_search',
-        {   query => {
-                filtered => {
-                    query  => { match_all => {} },
-                    filter => {
-                        and => [
-                            { term => { author => uc($id) } },
-                            { term => { status => 'latest' } }
-                        ]
-                    },
-                }
-            },
-            sort => [
-                'distribution', { 'version_numified' => { reverse => \1 } }
-            ],
-            fields => [qw(author distribution name status abstract date)],
-            size   => 1000,
-        }
-    );
+    my $author_cv = $c->model('API')->author->get($id);
+    my $releases_cv = $c->model('API')->release->latest_by_author($id);
 
     my ( $author, $releases ) = ( $author_cv & $releases_cv )->recv;
     $c->detach('/not_found') unless ( $author->{pauseid} );
 
     $c->stash(
-        {   author => $author,
-            releases =>
-                [ map { $_->{fields} } @{ $releases->{hits}->{hits} } ],
-            took     => $releases->{took},
-            total    => $releases->{hits}->{total},
+        {   author   => $author,
+            releases => $releases->fields,
+            took     => $releases->took,
+            total    => $releases->total,
             template => 'author.html'
         }
     );
diff --git a/lib/MetaCPAN/Web/Controller/Feed.pm b/lib/MetaCPAN/Web/Controller/Feed.pm
index 51552e92ac..ece4c33b85 100644
--- a/lib/MetaCPAN/Web/Controller/Feed.pm
+++ b/lib/MetaCPAN/Web/Controller/Feed.pm
@@ -6,6 +6,7 @@ use namespace::autoclean;
 BEGIN { extends 'MetaCPAN::Web::Controller' }
 use XML::Feed;
 use DateTime::Format::ISO8601;
+use AnyEvent;
 
 sub index : PathPart('feed') : Chained('/') : CaptureArgs(0) {
 }
@@ -32,10 +33,10 @@ sub author : Chained('index') : Path : Args(1) {
 
 sub distribution : Chained('index') : Path : Args(1) {
     my ( $self, $c, $distribution ) = @_;
-    my $data = $c->model('API::Release')->versions($distribution)->recv;
+    my $versions = $c->model('API')->release->versions($distribution)->recv;
     $c->stash->{feed} = $self->build_feed(
         title   => "Recent CPAN uploads of $distribution - MetaCPAN",
-        entries => [ map { $_->{fields} } @{ $data->{hits}->{hits} } ]
+        entries => $versions->fields,
     );
 }
 
diff --git a/lib/MetaCPAN/Web/Controller/Mirrors.pm b/lib/MetaCPAN/Web/Controller/Mirrors.pm
index ab6b205852..50b54eb259 100644
--- a/lib/MetaCPAN/Web/Controller/Mirrors.pm
+++ b/lib/MetaCPAN/Web/Controller/Mirrors.pm
@@ -21,28 +21,8 @@ sub index : Path {
         }
     }
 
-    my @or;
-    push( @or, { not => { filter => { missing => { field => $_ } } } } )
-        for (@protocols);
-
     my $cv   = AE::cv;
-    my $data = $c->model('API')->request(
-        '/mirror/_search',
-        {   size  => 999,
-            query => { match_all => {} },
-            @or ? ( filter => { and => \@or } ) : (),
-            $location
-            ? ( sort => {
-                    _geo_distance => {
-                        location => [ $location->[1], $location->[0] ],
-                        order    => "asc",
-                        unit     => "km"
-                    }
-                }
-                )
-            : ( sort => [ 'continent', 'country' ] )
-        }
-    )->recv;
+    my $data = $c->model('API')->mirror->list($location, \@protocols)->recv;
     my $latest = [
         map {
             {
@@ -50,12 +30,12 @@ sub index : Path {
                     ? $_->{sort}->[0]
                     : undef
             }
-            } @{ $data->{hits}->{hits} }
+            } @{ $data->hits }
     ];
     $c->stash(
         {   mirrors  => $latest,
-            took     => $data->{took},
-            total    => $data->{hits}->{total},
+            took     => $data->took,
+            total    => $data->total,
             template => 'mirrors.html',
         }
     );
diff --git a/lib/MetaCPAN/Web/Controller/Module.pm b/lib/MetaCPAN/Web/Controller/Module.pm
index 965d7e50ce..bbfc4551c2 100644
--- a/lib/MetaCPAN/Web/Controller/Module.pm
+++ b/lib/MetaCPAN/Web/Controller/Module.pm
@@ -9,18 +9,18 @@ sub index : PathPart('module') : Chained('/') : Args {
     my ( $self, $c, @module ) = @_;
     my $data
         = @module == 1
-        ? $c->model('API::Module')->find(@module)->recv
-        : $c->model('API::Module')->get(@module)->recv;
+        ? $c->model('API')->module->find(@module)->recv
+        : $c->model('API')->module->get(@module)->recv;
 
     $c->detach('/not_found') unless ( $data->{name} );
     my $pod = $c->model('API')->request( '/pod/' . join( '/', @module ) );
     my $release
-        = $c->model('API::Release')->get( $data->{author}, $data->{release} );
-    my $author = $c->model('API::Author')->get( $data->{author} );
+        = $c->model('API')->release->get( $data->{author}, $data->{release} );
+    my $author = $c->model('API')->author->get( $data->{author} );
     my $versions
-        = $c->model('API::Release')->versions( $data->{distribution} );
+        = $c->model('API')->release->versions( $data->{distribution} );
     my $favorites
-        = $c->model('API::Favorite')
+        = $c->model('API')->favorite
         ->get( $c->user_exists ? $c->user->pause_id : undef,
         $data->{distribution} );
     ( $pod, $author, $release, $versions, $favorites )
@@ -29,12 +29,11 @@ sub index : PathPart('module') : Chained('/') : Args {
     $data->{favorites}  = $favorites->{favorites}->{ $data->{distribution} };
 
     $c->stash(
-        {   module  => $data,
-            author  => $author,
-            pod     => $pod->{raw},
-            release => $release->{hits}->{hits}->[0]->{_source},
-            versions =>
-                [ map { $_->{fields} } @{ $versions->{hits}->{hits} } ],
+        {   module   => $data,
+            author   => $author,
+            pod      => $pod->{raw},
+            release  => $release->hits->[0]->{_source},
+            versions => $versions->fields,
             template => 'module.html',
         }
     );
diff --git a/lib/MetaCPAN/Web/Controller/Raw.pm b/lib/MetaCPAN/Web/Controller/Raw.pm
index 96d310506b..fbd6154857 100644
--- a/lib/MetaCPAN/Web/Controller/Raw.pm
+++ b/lib/MetaCPAN/Web/Controller/Raw.pm
@@ -9,8 +9,8 @@ sub index : PathPart('raw') : Chained('/') : Args {
     my ( $self, $c, @module ) = @_;
     my $req = $c->req;
     my ( $source, $module )
-        = ( $c->model('API::Module')->source(@module)
-            & $c->model('API::Module')->get(@module) )->recv;
+        = ( $c->model('API')->module->source(@module)
+            & $c->model('API')->module->get(@module) )->recv;
     $c->detach('/not_found') unless ( $source->{raw} );
     if ( $req->parameters->{download} ) {
         my $content_disposition = 'attachment';
diff --git a/lib/MetaCPAN/Web/Controller/Recent.pm b/lib/MetaCPAN/Web/Controller/Recent.pm
index 32d16f9c4f..fc4ccbce15 100644
--- a/lib/MetaCPAN/Web/Controller/Recent.pm
+++ b/lib/MetaCPAN/Web/Controller/Recent.pm
@@ -6,13 +6,13 @@ use base 'MetaCPAN::Web::Controller';
 sub index : Path {
     my ( $self, $c ) = @_;
     my $cv = AE::cv;
-    $c->model('API::Release')->recent( $c->req->page )->(
+    $c->model('API')->release->recent( $c->req->page )->(
         sub {
             my ($data) = shift->recv;
-            my $latest = [ map { $_->{_source} } @{ $data->{hits}->{hits} } ];
             $cv->send(
-                {   recent => $latest, took => $data->{took},
-                    total  => $data->{hits}->{total}
+                {   recent => $data->source,
+                    took   => $data->took,
+                    total  => $data->total
                 }
             );
         }
diff --git a/lib/MetaCPAN/Web/Controller/Release.pm b/lib/MetaCPAN/Web/Controller/Release.pm
index 74d3bc9e56..a08aeac35c 100644
--- a/lib/MetaCPAN/Web/Controller/Release.pm
+++ b/lib/MetaCPAN/Web/Controller/Release.pm
@@ -8,22 +8,22 @@ use List::Util ();
 
 sub index : PathPart('release') : Chained('/') : Args {
     my ( $self, $c, $author, $release ) = @_;
-    my $model = $c->model('API::Release');
+    my $model = $c->model('API')->release;
 
     my $data
         = $author && $release
         ? $model->get( $author, $release )
         : $model->find($author);
-    my $out = $data->recv->{hits}->{hits}->[0]->{_source};
+    my $out = $data->recv->hits->[0]->{_source};
     $c->detach('/not_found') unless ($out);
     ( $author, $release ) = ( $out->{author}, $out->{name} );
     my $modules = $model->modules( $author, $release );
     my $root = $model->root_files( $author, $release );
     my $versions = $model->versions( $out->{distribution} );
-    $author = $c->model('API::Author')->get($author);
+    $author = $c->model('API')->author->get($author);
     my $favorites
-        = $c->model('API::Favorite')
-        ->get( $c->user_exists ? $c->user->pause_id : undef,
+        = $c->model('API')
+        ->favorite->get( $c->user_exists ? $c->user->pause_id : undef,
         $out->{distribution} );
     ( $modules, $versions, $author, $root, $favorites )
         = ( $modules & $versions & $author & $root & $favorites )->recv;
@@ -34,24 +34,20 @@ sub index : PathPart('release') : Chained('/') : Args {
         {   template => 'release.html',
             release  => $out,
             author   => $author,
-            total    => $modules->{hits}->{total},
+            total    => $modules->total,
             took     => List::Util::max(
-                $modules->{took}, $root->{took}, $versions->{took}
+                $modules->took, $root->took, $versions->took
             ),
-            root => [
-                sort { $a->{name} cmp $b->{name} }
-                map  { $_->{fields} } @{ $root->{hits}->{hits} }
-            ],
-            versions =>
-                [ map { $_->{fields} } @{ $versions->{hits}->{hits} } ],
-            files => [
+            root => [ sort { $a->{name} cmp $b->{name} } @{ $root->fields } ],
+            versions => $versions->fields,
+            files    => [
                 map {
                     {
-                        %{ $_->{fields} },
-                            module   => $_->{fields}->{'_source.module'},
-                            abstract => $_->{fields}->{'_source.abstract'}
+                        %$_,
+                            module   => $_->{'_source.module'},
+                            abstract => $_->{'_source.abstract'}
                     }
-                    } @{ $modules->{hits}->{hits} }
+                    } @{ $modules->fields }
             ]
         }
     );
diff --git a/lib/MetaCPAN/Web/Controller/Root.pm b/lib/MetaCPAN/Web/Controller/Root.pm
index 7713eadd70..91e998ac35 100644
--- a/lib/MetaCPAN/Web/Controller/Root.pm
+++ b/lib/MetaCPAN/Web/Controller/Root.pm
@@ -73,8 +73,9 @@ Attempt to render a view, if needed.
 sub end : ActionClass('RenderView') {
     my ( $self, $c ) = @_;
     $c->stash->{req}        = $c->req;
-    $c->stash->{api}        = $c->config->{api};
-    $c->stash->{api_secure} = $c->config->{api_secure} || $c->config->{api};
+    $c->stash->{api}        = $c->config->{'Model::API'}{args}{url};
+    $c->stash->{api_secure} = $c->config->{'Model::API'}{args}{url_secure}
+        || $c->config->{'Model::API'}{args}{url};
 }
 
 =head1 AUTHOR
diff --git a/lib/MetaCPAN/Web/Controller/Search.pm b/lib/MetaCPAN/Web/Controller/Search.pm
index 680cd9e6f9..3730114afd 100644
--- a/lib/MetaCPAN/Web/Controller/Search.pm
+++ b/lib/MetaCPAN/Web/Controller/Search.pm
@@ -17,7 +17,7 @@ sub index : Path {
     my $query = join( ' ', @query );
     $query =~ s/::/ /g if ($query);
 
-    my $model = $c->model('API::Module');
+    my $model = $c->model('API')->module;
     my $from  = ( $req->page - 1 ) * 20;
     if ( $req->parameters->{lucky} ) {
         my $module = $model->first($query)->recv;
diff --git a/lib/MetaCPAN/Web/Controller/Search/AutoComplete.pm b/lib/MetaCPAN/Web/Controller/Search/AutoComplete.pm
index 8344550f76..0a608dae06 100644
--- a/lib/MetaCPAN/Web/Controller/Search/AutoComplete.pm
+++ b/lib/MetaCPAN/Web/Controller/Search/AutoComplete.pm
@@ -8,7 +8,7 @@ BEGIN { extends 'MetaCPAN::Web::Controller' }
 sub index : Path {
     my ( $self, $c ) = @_;
     my $req   = $c->req;
-    my $model = $c->model('API::Module');
+    my $model = $c->model('API')->module;
     my $query = join( ' ', $req->param('q') );
     $query =~ s/::/ /g if ($query);
 
diff --git a/lib/MetaCPAN/Web/Controller/Source.pm b/lib/MetaCPAN/Web/Controller/Source.pm
index 97ef8267b5..7efd1f7a6a 100644
--- a/lib/MetaCPAN/Web/Controller/Source.pm
+++ b/lib/MetaCPAN/Web/Controller/Source.pm
@@ -8,8 +8,8 @@ BEGIN { extends 'MetaCPAN::Web::Controller' }
 sub index : PathPart('source') : Chained('/') : Args {
     my ( $self, $c, @module ) = @_;
     my ( $source, $module )
-        = ( $c->model('API::Module')->source(@module)
-            & $c->model('API::Module')->get(@module) )->recv;
+        = ( $c->model('API')->module->source(@module)
+            & $c->model('API')->module->get(@module) )->recv;
     if ( $source->{raw} ) {
         $c->stash(
             {   template => 'source.html',
diff --git a/lib/MetaCPAN/Web/Model/API.pm b/lib/MetaCPAN/Web/Model/API.pm
index ebf571a5b2..6fb979c12a 100644
--- a/lib/MetaCPAN/Web/Model/API.pm
+++ b/lib/MetaCPAN/Web/Model/API.pm
@@ -1,69 +1,9 @@
 package MetaCPAN::Web::Model::API;
 
 use Moose;
-extends 'Catalyst::Model';
+use namespace::autoclean;
+extends 'Catalyst::Model::Adaptor';
 
-has [qw(api api_secure)] => ( is => 'ro' );
-
-use MetaCPAN::Web::MyCondVar;
-use Test::More;
-use JSON;
-use AnyEvent::HTTP qw(http_request);
-
-sub cv {
-    MetaCPAN::Web::MyCondVar->new;
-}
-
-=head2 COMPONENT
-
-Set C<api> and C<api_secure> config parameters from the app config object.
-
-=cut
-
-sub COMPONENT {
-    my $self = shift;
-    my ( $app, $config ) = @_;
-    $config = $self->merge_config_hashes(
-        {   api        => $app->config->{api},
-            api_secure => $app->config->{api_secure} || $app->config->{api}
-        },
-        $config
-    );
-    return $self->SUPER::COMPONENT( $app, $config );
-}
-
-sub model {
-    my ( $self, $model ) = @_;
-    return MetaCPAN::Web->model('API') unless $model;
-    return MetaCPAN::Web->model("API::$model");
-}
-
-sub request {
-    my ( $self, $path, $search, $params ) = @_;
-    my ( $token, $method ) = @$params{qw(token method)};
-    $path .= "?access_token=$token" if ($token);
-    my $req = $self->cv;
-    http_request $method ? $method
-        : $search        ? 'post'
-        : 'get' => ( $token ? $self->api_secure : $self->api ) . $path,
-        body => $search ? encode_json($search) : undef,
-        headers    => { 'Content-type' => 'application/json' },
-        persistent => 1,
-        sub {
-        my ( $data, $headers ) = @_;
-        my $content_type = $headers->{'content-type'} || '';
-
-        if ( $content_type =~ /^application\/json/ ) {
-            my $json = eval { decode_json($data) };
-            $req->send( $@ ? { raw => $data } : $json );
-        }
-        else {
-
-            # Response is raw data, e.g. text/plain
-            $req->send( { raw => $data } );
-        }
-        };
-    return $req;
-}
+__PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/lib/MetaCPAN/Web/Model/API/Author.pm b/lib/MetaCPAN/Web/Model/API/Author.pm
deleted file mode 100644
index 5f82fe93e7..0000000000
--- a/lib/MetaCPAN/Web/Model/API/Author.pm
+++ /dev/null
@@ -1,34 +0,0 @@
-package MetaCPAN::Web::Model::API::Author;
-use Moose;
-use namespace::autoclean;
-
-extends 'MetaCPAN::Web::Model::API';
-
-=head1 NAME
-
-MetaCPAN::Web::Model::Author - Catalyst Model
-
-=head1 DESCRIPTION
-
-Catalyst Model.
-
-=head1 AUTHOR
-
-Matthew Phillips
-
-=head1 LICENSE
-
-This library is free software. You can redistribute it and/or modify
-it under the same terms as Perl itself.
-
-=cut
-
-sub get {
-    my ( $self, $author ) = @_;
-    $self->request( "/author/" . uc($author) );
-
-}
-
-__PACKAGE__->meta->make_immutable;
-
-1;
diff --git a/lib/MetaCPAN/Web/User.pm b/lib/MetaCPAN/Web/User.pm
index 1c9c088c1a..6014f27633 100644
--- a/lib/MetaCPAN/Web/User.pm
+++ b/lib/MetaCPAN/Web/User.pm
@@ -28,7 +28,7 @@ sub for_session {
 
 sub from_session {
     my ( $self, $c, $id ) = @_;
-    my $user = $c->model('API::User')->get($id)->recv;
+    my $user = $c->model('API')->user->get($id)->recv;
     $self->obj( Hash::AsObject->new($user) ) if ($user);
     return $user ? $self : undef;
 }
@@ -36,7 +36,7 @@ sub from_session {
 sub find_user {
     my ( $self, $auth, $c ) = @_;
     my $obj = Hash::AsObject->new(
-        $c->model('API::User')->get( $auth->{token} )->recv );
+        $c->model('API')->user->get( $auth->{token} )->recv );
     $self->obj($obj);
     return $self;
 }
diff --git a/metacpan_web.conf b/metacpan_web.conf
index 336cfc8261..3404d7d6cb 100644
--- a/metacpan_web.conf
+++ b/metacpan_web.conf
@@ -3,11 +3,17 @@
 name MetaCPAN::Web
 default_view HTML
 
-api http://api.metacpan.org
-api_secure https://api.metacpan.org
 consumer_key = metacpan.dev
 consumer_secret = ClearAirTurbulence
 
+<model API>
+    class MetaCPAN::Web::API
+    <args>
+        url http://api.metacpan.org
+        url_secure https://api.metacpan.org
+    </args>
+</model>
+
 <view HTML>
   INCLUDE_PATH root/
   TAG_STYLE asp
