diff --git a/examples/spotify-api-example/.gitignore b/examples/spotify-api-example/.gitignore
new file mode 100644
index 000000000..9f11b755a
--- /dev/null
+++ b/examples/spotify-api-example/.gitignore
@@ -0,0 +1 @@
+.idea/
diff --git a/examples/spotify-api-example/README.md b/examples/spotify-api-example/README.md
new file mode 100644
index 000000000..fec687e85
--- /dev/null
+++ b/examples/spotify-api-example/README.md
@@ -0,0 +1,28 @@
+## A simple example processing data from the Spotify API
+
+### Build
+`mvn package`
+
+### Run
+`java -jar target/spotify-api-example-service.jar`
+
+### Call
+```
+$ http :8080/albums/new
+HTTP/1.1 200 OK
+Content-Length: 1364
+Date: Thu, 03 Dec 2015 16:30:18 GMT
+Server: Jetty(9.3.4.v20151007)
+
+[{"name":"Hör vad du säger men jag har glömt vad du sa","artist":{"name":"Danny Saucedo"}},{"name":"The Only One (Kleerup Remix)","artist":{"name":"Miriam Bryant"}},{"name":"Broken Arrows (Remixes)","artist":{"name":"Avicii"}},{"name":"Handwritten (Revisited)","artist":{"name":"Shawn Mendes"}},{"name":"Jag går nu","artist":{"name":"Melissa Horn"}},{"name":"Bang My Head (feat. Sia & Fetty Wap)","artist":{"name":"David Guetta"}},{"name":"Handwritten (Revisited)","artist":{"name":"Shawn Mendes"}},{"name":"Everglow","artist":{"name":"Coldplay"}},{"name":"Jag hör vad du säger men glömt vad du sa","artist":{"name":"Danny Saucedo"}},{"name":"Regissören","artist":{"name":"Dani M"}},{"name":"Peace Is The Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Stay","artist":{"name":"Kygo"}},{"name":"Fine By Me","artist":{"name":"Chris Brown"}},{"name":"Be Together","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission (Extended)","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Peace is the Mission: Extended","artist":{"name":"Major Lazer"}},{"name":"Peace Is The Mission : Extended","artist":{"name":"Major Lazer"}},{"name":"Adventure Of A Lifetime (Radio Edit)","artist":{"name":"Coldplay"}}]
+```
+
+```
+$ http :8080/artists/toptracks/se?q=elvis
+HTTP/1.1 200 OK
+Content-Length: 1110
+Date: Thu, 03 Dec 2015 16:31:29 GMT
+Server: Jetty(9.3.4.v20151007)
+
+[{"name":"Can't Help Falling in Love","album":{"name":"Blue Hawaii","artist":{"name":"Elvis Presley"}}},{"name":"Blue Christmas","album":{"name":"Elvis' Christmas Album","artist":{"name":"Elvis Presley"}}},{"name":"Jailhouse Rock","album":{"name":"Elvis' Golden Records","artist":{"name":"Elvis Presley"}}},{"name":"Suspicious Minds","album":{"name":"Back In Memphis","artist":{"name":"Elvis Presley"}}},{"name":"A Little Less Conversation - JXL Radio Edit Remix","album":{"name":"Elvis 75 - Good Rockin' Tonight","artist":{"name":"Elvis Presley"}}},{"name":"Always on My Mind - Remastered","album":{"name":"The Essential Elvis Presley","artist":{"name":"Elvis Presley"}}},{"name":"Here Comes Santa Claus (Right Down Santa Claus Lane)","album":{"name":"Elvis' Christmas Album","artist":{"name":"Elvis Presley"}}},{"name":"In the Ghetto","album":{"name":"From Elvis In Memphis","artist":{"name":"Elvis Presley"}}},{"name":"Hound Dog","album":{"name":"Elvis' Golden Records","artist":{"name":"Elvis Presley"}}},{"name":"Don't Be Cruel","album":{"name":"Elvis' Golden Records","artist":{"name":"Elvis Presley"}}}]
+```
diff --git a/examples/spotify-api-example/pom.xml b/examples/spotify-api-example/pom.xml
new file mode 100644
index 000000000..01960d140
--- /dev/null
+++ b/examples/spotify-api-example/pom.xml
@@ -0,0 +1,113 @@
+
+
+ 4.0.0
+
+ Spotify API Example
+ An Apollo example application using the Spotify API
+ com.spotify
+ spotify-api-example-service
+ 0.0.1-SNAPSHOT
+ jar
+
+
+
+
+ com.spotify
+ apollo-bom
+ 1.0.0
+ pom
+ import
+
+
+
+
+
+
+ com.spotify
+ apollo-http-service
+
+
+
+ com.spotify
+ apollo-extra
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.1.3
+
+
+
+
+ ${project.artifactId}
+
+
+ maven-enforcer-plugin
+ 1.4.1
+
+
+ enforce
+
+
+
+
+
+
+ enforce
+
+
+
+
+
+
+ maven-compiler-plugin
+ 3.1
+
+ 1.8
+ 1.8
+
+ -Xlint:all
+
+
+
+
+
+ maven-dependency-plugin
+ 2.10
+
+
+ prepare-package
+
+ copy-dependencies
+
+
+
+
+ false
+ false
+ true
+ runtime
+ ${project.build.directory}/lib
+
+
+
+
+ maven-jar-plugin
+ 2.6
+
+
+ true
+
+ true
+ true
+ true
+ lib/
+ com.spotify.apollo.example.SpotifyApiExample
+
+
+
+
+
+
+
diff --git a/examples/spotify-api-example/src/main/java/com/spotify/apollo/example/AlbumResource.java b/examples/spotify-api-example/src/main/java/com/spotify/apollo/example/AlbumResource.java
new file mode 100644
index 000000000..be9335d13
--- /dev/null
+++ b/examples/spotify-api-example/src/main/java/com/spotify/apollo/example/AlbumResource.java
@@ -0,0 +1,113 @@
+package com.spotify.apollo.example;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.spotify.apollo.Client;
+import com.spotify.apollo.Request;
+import com.spotify.apollo.RequestContext;
+import com.spotify.apollo.Response;
+import com.spotify.apollo.example.data.Album;
+import com.spotify.apollo.example.data.Artist;
+import com.spotify.apollo.route.AsyncHandler;
+import com.spotify.apollo.route.JsonSerializerMiddlewares;
+import com.spotify.apollo.route.Route;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.StringJoiner;
+import java.util.concurrent.CompletionStage;
+import java.util.stream.Stream;
+
+import okio.ByteString;
+
+/**
+ * The album resource demonstrates how to use asynchronous routes in Apollo.
+ */
+public class AlbumResource {
+
+ private static final String SPOTIFY_API = "https://api.spotify.com";
+ private static final String SEARCH_API = SPOTIFY_API + "/v1/search";
+ private static final String ALBUM_API = SPOTIFY_API + "/v1/albums";
+
+ private final ObjectMapper objectMapper;
+ private final ObjectWriter objectWriter;
+
+ public AlbumResource(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ this.objectWriter = objectMapper.writer();
+ }
+
+ public Stream>>> routes() {
+ // The album resource has two routes. Since both are returning the same type,
+ // we can map them to the same middleware
+ // Note that this could also have been set up as a single route to "/albums/".
+ return Stream.of(
+ Route.async("GET", "/albums/new", context -> getAlbums(context, "new")),
+ Route.async("GET", "/albums/hipster", context -> getAlbums(context, "hipster"))
+ )
+ .map(route -> route.withMiddleware(
+ JsonSerializerMiddlewares.jsonSerializeResponse(objectWriter)));
+ }
+
+ public CompletionStage>> getAlbums(RequestContext requestContext, String tag) {
+ // We need to first query the search API, parse the result, then query the album API.
+ Request searchRequest = Request.forUri(SEARCH_API + "?type=album&q=tag%3A" + tag);
+ Client client = requestContext.requestScopedClient();
+ return client
+ .send(searchRequest)
+ .thenComposeAsync(response -> {
+ String ids = parseResponseAlbumIds(response.payload().get().utf8());
+ return client.send(Request.forUri(ALBUM_API + "?ids=" + ids));
+ })
+ .thenApplyAsync(response -> Response.ok()
+ .withPayload(parseAlbumData(response.payload().get().utf8())));
+ }
+
+ /**
+ * Parses an album response from a
+ * Spotify API album query.
+ *
+ * @param json The json response
+ * @return A list of albums with artist information
+ */
+ private ArrayList parseAlbumData(String json) {
+ ArrayList albums = new ArrayList<>();
+ try {
+ JsonNode jsonNode = this.objectMapper.readTree(json);
+ for (JsonNode albumNode : jsonNode.get("albums")) {
+ JsonNode artistsNode = albumNode.get("artists");
+ // Exclude albums with 0 artists
+ if (artistsNode.size() >= 1) {
+ // Only keeping the first artist for simplicity
+ Artist artist = new Artist(artistsNode.get(0).get("name").asText());
+ Album album = new Album(albumNode.get("name").asText(), artist);
+ albums.add(album);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to parse JSON", e);
+ }
+ return albums;
+ }
+
+ /**
+ * Parses the album ids from a JSON response from a
+ * Spotify API search query.
+ *
+ * @param json The JSON response
+ * @return A comma-separated list of album ids from the response
+ */
+ private String parseResponseAlbumIds(String json) {
+ StringJoiner sj = new StringJoiner(",");
+ try {
+ JsonNode jsonNode = this.objectMapper.readTree(json);
+ for (JsonNode node : jsonNode.get("albums").get("items")) {
+ sj.add(node.get("id").asText());
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to parse JSON", e);
+ }
+ return sj.toString();
+ }
+}
diff --git a/examples/spotify-api-example/src/main/java/com/spotify/apollo/example/ArtistResource.java b/examples/spotify-api-example/src/main/java/com/spotify/apollo/example/ArtistResource.java
new file mode 100644
index 000000000..758dfced6
--- /dev/null
+++ b/examples/spotify-api-example/src/main/java/com/spotify/apollo/example/ArtistResource.java
@@ -0,0 +1,123 @@
+package com.spotify.apollo.example;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.spotify.apollo.Client;
+import com.spotify.apollo.Request;
+import com.spotify.apollo.RequestContext;
+import com.spotify.apollo.Response;
+import com.spotify.apollo.Status;
+import com.spotify.apollo.example.data.Album;
+import com.spotify.apollo.example.data.Artist;
+import com.spotify.apollo.example.data.Track;
+import com.spotify.apollo.route.AsyncHandler;
+import com.spotify.apollo.route.JsonSerializerMiddlewares;
+import com.spotify.apollo.route.Route;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.stream.Stream;
+
+import okio.ByteString;
+
+/**
+ * The artist resource demonstrates how to use asynchronous routes in Apollo with query arguments.
+ */
+public class ArtistResource {
+
+ private static final String SPOTIFY_API = "https://api.spotify.com";
+ private static final String SEARCH_API = SPOTIFY_API + "/v1/search";
+ private static final String ARTIST_API = SPOTIFY_API + "/v1/artists";
+
+ private final ObjectMapper objectMapper;
+ private final ObjectWriter objectWriter;
+
+ public ArtistResource(ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ this.objectWriter = objectMapper.writer();
+ }
+
+ public Stream>>> routes() {
+ // The artist resource has one parameterized route.
+ return Stream.of(
+ Route.async("GET", "/artists/toptracks/", this::getArtistTopTracks)
+ .withMiddleware(JsonSerializerMiddlewares.jsonSerializeResponse(objectWriter))
+ );
+ }
+
+ public CompletionStage>> getArtistTopTracks(RequestContext requestContext) {
+ // Validate request
+ Optional query = requestContext.request().parameter("q");
+ if (!query.isPresent()) {
+ return CompletableFuture.completedFuture(
+ Response.forStatus(Status.BAD_REQUEST.withReasonPhrase("No search query")));
+ }
+ String country = requestContext.pathArgs().get("country");
+
+ // We need to first query the search API, parse the result, then query top-tracks.
+ Request searchRequest = Request.forUri(SEARCH_API + "?type=artist&q=" + query.get());
+ Client client = requestContext.requestScopedClient();
+ return client
+ .send(searchRequest)
+ .thenComposeAsync(response -> {
+ String topArtistId = parseFirstArtistId(response.payload().get().utf8());
+ Request topTracksRequest = Request.forUri(
+ String.format("%s/%s/top-tracks?country=%s", ARTIST_API, topArtistId, country));
+ return client.send(topTracksRequest);
+ })
+ .thenApplyAsync(response -> Response.ok()
+ .withPayload(parseTopTracks(response.payload().get().utf8())))
+ // This .exceptionally doesn't provide any additional value,
+ // but it shows how you could handle exceptions
+ .exceptionally(throwable -> Response
+ .forStatus(Status.INTERNAL_SERVER_ERROR.withReasonPhrase("Something failed")));
+ }
+
+ /**
+ * Parses an artist top tracks response from the
+ * Spotify API
+ *
+ * @param json The json response
+ * @return A list of top tracks
+ */
+ private ArrayList