diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index bbc4e2e..622ce3d 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -72,7 +72,9 @@ public function build(ContainerBuilder $container) {
       ->addArgument(new Reference('database'));
     $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
       ->addArgument(new Reference('router.dumper'))
-      ->addArgument(new Reference('lock'));
+      ->addArgument(new Reference('lock'))
+      ->addArgument(new Reference('dispatcher'));
+
 
     $container->register('matcher', 'Drupal\Core\Routing\ChainMatcher');
     $container->register('legacy_url_matcher', 'Drupal\Core\LegacyUrlMatcher')
diff --git a/core/lib/Drupal/Core/Routing/RouteBuildEvent.php b/core/lib/Drupal/Core/Routing/RouteBuildEvent.php
new file mode 100644
index 0000000..017b83a
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RouteBuildEvent.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\RouteBuildEvent.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Represents route building information as event.
+ */
+class RouteBuildEvent extends Event {
+
+  /**
+   * The route collection.
+   *
+   * @var \Symfony\Component\Routing\RouteCollection
+   */
+  protected $routeCollection;
+
+  /**
+   * The module name that provides the route.
+   *
+   * @var string
+   */
+  protected $module;
+
+  /**
+   * Constructs a RouteBuildEvent object.
+   */
+  public function __construct(RouteCollection $route_collection, $module) {
+    $this->routeCollection = $route_collection;
+    $this->module = $module;
+  }
+
+  /**
+   * Gets the route collection.
+   */
+  public function getRouteCollection() {
+    return $this->routeCollection;
+  }
+
+  /**
+   * Gets the module that provides the route.
+   */
+  public function getModule() {
+    return $this->module;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/RouteBuilder.php b/core/lib/Drupal/Core/Routing/RouteBuilder.php
index 3a27767..fc12ee8 100644
--- a/core/lib/Drupal/Core/Routing/RouteBuilder.php
+++ b/core/lib/Drupal/Core/Routing/RouteBuilder.php
@@ -7,8 +7,13 @@
 
 namespace Drupal\Core\Routing;
 
-use Drupal\Core\Lock\LockBackendInterface;
 use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Yaml\Parser;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+
+use Drupal\Core\Lock\LockBackendInterface;
 
 /**
  * Managing class for rebuilding the router table.
@@ -33,16 +38,26 @@ class RouteBuilder {
   protected $lock;
 
   /**
+   * The event dispatcher to notify of routes.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $dispatcher;
+
+  /**
    * Construcs the RouteBuilder using the passed MatcherDumperInterface.
    *
    * @param \Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface $dumper
    *   The matcher dumper used to store the route information.
    * @param \Drupal\Core\Lock\LockBackendInterface $lock
    *   The lock backend.
+   * @param \Symfony\Component\EventDispatcherEventDispatcherInterface
+   *   The event dispatcher to notify of routes.
    */
-  public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock) {
+  public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock, EventDispatcherInterface $dispatcher) {
     $this->dumper = $dumper;
     $this->lock = $lock;
+    $this->dispatcher = $dispatcher;
   }
 
   /**
@@ -57,15 +72,38 @@ public function rebuild() {
       return;
     }
 
+    $parser = new Parser();
+
     // We need to manually call each module so that we can know which module
     // a given item came from.
-
-    foreach (module_implements('route_info') as $module) {
-      $routes = call_user_func($module . '_route_info');
-      drupal_alter('router_info', $routes, $module);
-      $this->dumper->addRoutes($routes);
+    // @todo Use an injected Extension service rather than module_list():
+    //   http://drupal.org/node/1331486.
+    foreach (module_list() as $module) {
+      $collection = new RouteCollection();
+      $routing_file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . '/' . $module . '.routing.yml';
+      if (file_exists($routing_file)) {
+        $routes = $parser->parse(file_get_contents($routing_file));
+        if (!empty($routes)) {
+          foreach ($routes as $name => $route_info) {
+            $defaults = isset($route_info['defaults']) ? $route_info['defaults'] : array();
+            $requirements = isset($route_info['requirements']) ? $route_info['requirements'] : array();
+            $route = new Route($route_info['pattern'], $defaults, $requirements);
+            $collection->add($name, $route);
+          }
+        }
+      }
+      $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection, $module));
+      $this->dumper->addRoutes($collection);
       $this->dumper->dump(array('route_set' => $module));
     }
+
+    // Now allow modules to register additional, dynamic routes.
+    $collection = new RouteCollection();
+    $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection, 'dynamic_routes'));
+    $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection, 'dynamic_routes'));
+    $this->dumper->addRoutes($collection);
+    $this->dumper->dump(array('route_set' => 'dynamic_routes'));
+
     $this->lock->release('router_rebuild');
   }
 
diff --git a/core/lib/Drupal/Core/Routing/RoutingEvents.php b/core/lib/Drupal/Core/Routing/RoutingEvents.php
new file mode 100644
index 0000000..3ca6ef6
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RoutingEvents.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\Core\Routing\RoutingEvents.
+ */
+
+namespace Drupal\Core\Routing;
+
+/**
+ * Contains all events thrown in the core routing component.
+ */
+final class RoutingEvents {
+
+  /**
+   * The ALTER event is fired on a route collection to allow changes to routes.
+   *
+   * This event is used to process new routes before they get saved.
+   *
+   * @see \Drupal\Core\Routing\RouteBuildEvent
+   *
+   * @var string
+   */
+  const ALTER = 'routing.route_alter';
+
+  /**
+   * The DYNAMIC event is fired to allow modules to register additional routes.
+   *
+   * Most routes are static, an should be defined as such. Dynamic routes are
+   * only those whose existence changes depending on the state of the system
+   * at runtime, depending on configuration.
+   *
+   * @see \Drupal\Core\Routing\RouteBuildEvent
+   *
+   * @var string
+   */
+  const DYNAMIC = 'routing.route_dynamic';
+}
diff --git a/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php b/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php
new file mode 100644
index 0000000..136eda1
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Definition of \Drupal\rest\EventSubscriber\RouteSubscriber.
+ */
+
+namespace Drupal\rest\EventSubscriber;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Drupal\rest\Plugin\Type\ResourcePluginManager;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Subscriber for REST-style routes.
+ */
+class RouteSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The plugin manager for REST plugins.
+   *
+   * @var \Drupal\rest\Plugin\Type\ResourcePluginManager
+   */
+  protected $manager;
+
+  /**
+   * The Drupal configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $config;
+
+  /**
+   * Constructs a RouteSubscriber object.
+   *
+   * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $manager
+   *   The resource plugin manager.
+   * @param \Drupal\Core\Config\ConfigFactory $config
+   *   The configuration factory holding resource settings.
+   */
+  public function __construct(ResourcePluginManager $manager, ConfigFactory $config) {
+    $this->manager = $manager;
+    $this->config = $config;
+  }
+
+  /**
+   * Adds routes to enabled REST resources.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route building event.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The route collection that contains the new dynamic routes.
+   */
+  public function dynamicRoutes(RouteBuildEvent $event) {
+
+    $collection = $event->getRouteCollection();
+
+    $resources = $this->config->get('rest')->load()->get('resources');
+    if ($resources && $enabled = array_intersect_key($this->manager->getDefinitions(), $resources)) {
+      foreach ($enabled as $key => $resource) {
+        $plugin = $this->manager->getInstance(array('id' => $key));
+
+        // @todo Switch to ->addCollection() once http://drupal.org/node/1819018 is resolved.
+        foreach ($plugin->routes() as $name => $route) {
+          $collection->add("rest.$name", $route);
+        }
+      }
+    }
+  }
+
+  /**
+   * Implements EventSubscriberInterface::getSubscribedEvents().
+   */
+  static function getSubscribedEvents() {
+    $events[RoutingEvents::DYNAMIC] = 'dynamicRoutes';
+    return $events;
+  }
+}
+
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
index 962af71..8ca8fcd 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/ResourceBase.php
@@ -17,6 +17,13 @@
 abstract class ResourceBase extends PluginBase {
 
   /**
+   * Holds all predefined HTTP methods and their configuration.
+   *
+   * @var array
+   */
+  protected $requestMethods;
+
+  /**
    * Provides an array of permissions suitable for hook_permission().
    *
    * Every plugin operation method gets its own user permission. Example:
@@ -29,7 +36,7 @@
   public function permissions() {
     $permissions = array();
     $definition = $this->getDefinition();
-    foreach ($this->requestMethods() as $method) {
+    foreach ($this->requestMethods() as $method => $settings) {
       $lowered_method = strtolower($method);
       // Only expose permissions where the HTTP request method exists on the
       // plugin.
@@ -55,7 +62,7 @@ public function routes() {
     $collection = new RouteCollection();
 
     $methods = $this->requestMethods();
-    foreach ($methods as $method) {
+    foreach ($methods as $method => $settings) {
       // Only expose routes where the HTTP request method exists on the plugin.
       if (method_exists($this, strtolower($method))) {
         $prefix = strtr($this->plugin_id, ':', '/');
@@ -83,22 +90,73 @@ public function routes() {
    * Provides predefined HTTP request methods.
    *
    * Plugins can override this method to provide additional custom request
-   * methods.
+   * methods or to alter the semantics.
+   *
+   * @param string $method
+   *   Optional: a specific request method to return setting information for.
    *
    * @return array
-   *   The list of allowed HTTP request method strings.
+   *   An array with HTTP methods as keys and value arrays with the following
+   *   key/value pairs:
+   *    - success_code: an integer used for the HTTP response code on success.
+   *    - incoming_data: a boolean to indicate that incoming data has to be
+   *      de-serialized.
+   *    - outgoing_data: a boolean to indicate that outgoing data has to be
+   *      serialized.
    */
-  protected function requestMethods() {
-    return drupal_map_assoc(array(
-      'HEAD',
-      'GET',
-      'POST',
-      'PUT',
-      'DELETE',
-      'TRACE',
-      'OPTIONS',
-      'CONNECT',
-      'PATCH',
-    ));
+  public function requestMethods($method = NULL) {
+    if ($this->requestMethods == NULL) {
+      $this->requestMethods = array(
+        'HEAD' => array(
+          'success_code' => 200,
+          'incoming_data' => FALSE,
+          'outgoing_data' => FALSE,
+        ),
+        'GET' => array(
+          'success_code' => 200,
+          'incoming_data' => FALSE,
+          'outgoing_data' => TRUE,
+        ),
+        'POST' => array(
+          'success_code' => 201,
+          'incoming_data' => TRUE,
+          'outgoing_data' => FALSE,
+        ),
+        'PUT' => array(
+          'success_code' => 200,
+          'incoming_data' => TRUE,
+          'outgoing_data' => FALSE,
+        ),
+        'DELETE' => array(
+          'success_code' => 204,
+          'incoming_data' => FALSE,
+          'outgoing_data' => FALSE,
+        ),
+        'TRACE' => array(
+          'success_code' => 200,
+          'incoming_data' => FALSE,
+          'outgoing_data' => FALSE,
+        ),
+        'OPTIONS' => array(
+          'success_code' => 200,
+          'incoming_data' => FALSE,
+          'outgoing_data' => FALSE,
+        ),
+        'CONNECT'  => array(
+          'success_code' => 200,
+          'incoming_data' => FALSE,
+          'outgoing_data' => FALSE,
+        ),
+        'PATCH'  => array(
+          'success_code' => 200,
+          'incoming_data' => TRUE,
+          'outgoing_data' => FALSE,
+        ),
+      );
+    }
+    if ($method) {
+      return $this->requestMethods[$method];
+    }
+    return $this->requestMethods;
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
index afc8d50..71c3d1c 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/DBLogResource.php
@@ -9,15 +9,15 @@
 
 use Drupal\rest\Plugin\ResourceBase;
 use Drupal\Core\Annotation\Plugin;
-use Symfony\Component\HttpFoundation\Response;
+use Drupal\Core\Annotation\Translation;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
  * Provides a resource for database watchdog log entries.
  *
  * @Plugin(
- *  id = "dblog",
- *  label = "Watchdog database log"
+ *   id = "dblog",
+ *   label = @Translation("Watchdog database log")
  * )
  */
 class DBLogResource extends ResourceBase {
@@ -38,8 +38,8 @@ public function routes() {
    *
    * Returns a watchdog log entry for the specified ID.
    *
-   * @return \Symfony\Component\HttpFoundation\Response
-   *   The response object.
+   * @return object
+   *   The databse row loaded as stdClass object.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
@@ -53,8 +53,7 @@ public function get($id = NULL) {
       if (empty($result)) {
         throw new NotFoundHttpException('Not Found');
       }
-      // @todo remove hard coded format here.
-      return new Response(drupal_json_encode($result[0]), 200, array('Content-Type' => 'application/json'));
+      return $result[0];
     }
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
index 3524389..a28ae07 100644
--- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php
@@ -8,6 +8,7 @@
 namespace Drupal\rest\Plugin\rest\resource;
 
 use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
 use Drupal\Core\Entity\EntityStorageException;
 use Drupal\rest\Plugin\ResourceBase;
 use Symfony\Component\HttpFoundation\Response;
@@ -18,21 +19,38 @@
  * Represents entities as resources.
  *
  * @Plugin(
- *  id = "entity",
- *  label = "Entity",
- *  derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
+ *   id = "entity",
+ *   label = @Translation("Entity"),
+ *   derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
  * )
  */
 class EntityResource extends ResourceBase {
 
   /**
-   * Responds to entity DELETE requests.
+   * Responds to entity GET requests.
    *
    * @param mixed $id
    *   The entity ID.
    *
-   * @return \Symfony\Component\HttpFoundation\Response
-   *   The response object.
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The loaded entity.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
+   */
+  public function get($id) {
+    $definition = $this->getDefinition();
+    $entity = entity_load($definition['entity_type'], $id);
+    if ($entity) {
+      return $entity;
+    }
+    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
+  }
+
+  /**
+   * Responds to entity DELETE requests.
+   *
+   * @param mixed $id
+   *   The entity ID.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
@@ -42,13 +60,13 @@ public function delete($id) {
     if ($entity) {
       try {
         $entity->delete();
-        // Delete responses have an empty body.
-        return new Response('', 204);
       }
       catch (EntityStorageException $e) {
         throw new HttpException(500, 'Internal Server Error', $e);
       }
     }
-    throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
+    else {
+      throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
+    }
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/RequestHandler.php b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
index 6bea36a..1dbd81a 100644
--- a/core/modules/rest/lib/Drupal/rest/RequestHandler.php
+++ b/core/modules/rest/lib/Drupal/rest/RequestHandler.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\rest;
 
+use Drupal\Core\Entity\EntityNG;
 use Symfony\Component\DependencyInjection\ContainerAware;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -33,17 +34,41 @@ class RequestHandler extends ContainerAware {
    *   '_plugin' property of route object.
    */
   public function handle($plugin, Request $request, $id = NULL) {
-    $method = strtolower($request->getMethod());
-    if (user_access("restful $method $plugin")) {
+    $method = $request->getMethod();
+    $lower_method = strtolower($method);
+    if (user_access("restful $lower_method $plugin")) {
       $resource = $this->container
         ->get('plugin.manager.rest')
         ->getInstance(array('id' => $plugin));
+      // @todo De-serialization should happen here if the request is supposed
+      // to carry incoming data.
+      $settings = $resource->requestMethods($method);
       try {
-        return $resource->{$method}($id);
+        $data = $resource->{$lower_method}($id);
       }
       catch (HttpException $e) {
         return new Response($e->getMessage(), $e->getStatusCode(), $e->getHeaders());
       }
+      // If the plugin returns response objects itself we pass them through.
+      if ($data instanceof Response) {
+        return $data;
+      }
+      // If there is no response body data to return we can send a response
+      // immediately.
+      if (empty($settings['outgoing_data'])) {
+        return new Response('', $settings['success_code']);
+      }
+      // Otherwise we serialize the data.
+      $serializer = $this->container->get('serializer');
+      // Convert object to array as normalizer does not support
+      // non EntityNG objects.
+      if (is_object($data) && !$data instanceof EntityNG) {
+        $data = (array) $data;
+      }
+      // @todo Replace the format here with something we get from the HTTP
+      //   Accept headers. See http://drupal.org/node/1833440
+      $output = $serializer->serialize($data, 'drupal_jsonld');
+      return new Response($output, $settings['success_code'], array('Content-Type' => 'application/ld+json'));
     }
     return new Response('Access Denied', 403);
   }
diff --git a/core/modules/rest/lib/Drupal/rest/RestBundle.php b/core/modules/rest/lib/Drupal/rest/RestBundle.php
index 7810c30..67d3c59 100644
--- a/core/modules/rest/lib/Drupal/rest/RestBundle.php
+++ b/core/modules/rest/lib/Drupal/rest/RestBundle.php
@@ -8,6 +8,7 @@
 namespace Drupal\rest;
 
 use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 
 /**
@@ -22,5 +23,10 @@ public function build(ContainerBuilder $container) {
     // Register the resource manager class with the dependency injection
     // container.
     $container->register('plugin.manager.rest', 'Drupal\rest\Plugin\Type\ResourcePluginManager');
+
+    $container->register('rest.route_subscriber', 'Drupal\rest\EventSubscriber\RouteSubscriber')
+      ->addArgument(new Reference('plugin.manager.rest'))
+      ->addArgument(new Reference('config.factory'))
+      ->addTag('event_subscriber');
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
index 6bd8e65..66b3457 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DBLogTest.php
@@ -19,7 +19,7 @@ class DBLogTest extends RESTTestBase {
    *
    * @var array
    */
-  public static $modules = array('rest', 'dblog');
+  public static $modules = array('jsonld', 'rest', 'dblog');
 
   public static function getInfo() {
     return array(
@@ -32,17 +32,7 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
     // Enable web API for the watchdog resource.
-    $config = config('rest');
-    $config->set('resources', array(
-      'dblog' => 'dblog',
-    ));
-    $config->save();
-
-    // Rebuild routing cache, so that the web API paths are available.
-    drupal_container()->get('router.builder')->rebuild();
-    // Reset the Simpletest permission cache, so that the new resource
-    // permissions get picked up.
-    drupal_static_reset('checkPermissions');
+    $this->enableService('dblog');
   }
 
   /**
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
index 187c386..4c039c2 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php
@@ -34,18 +34,7 @@ public static function getInfo() {
    */
   public function testDelete() {
     foreach (entity_get_info() as $entity_type => $info) {
-      // Enable web API for this entity type.
-      $config = config('rest');
-      $config->set('resources', array(
-        'entity:' . $entity_type => 'entity:' . $entity_type,
-      ));
-      $config->save();
-
-      // Rebuild routing cache, so that the web API paths are available.
-      drupal_container()->get('router.builder')->rebuild();
-      // Reset the Simpletest permission cache, so that the new resource
-      // permissions get picked up.
-      drupal_static_reset('checkPermissions');
+      $this->enableService('entity:' . $entity_type);
       // Create a user account that has the required permissions to delete
       // resources via the web API.
       $account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type));
@@ -81,6 +70,7 @@ public function testDelete() {
       $this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
     }
     // Try to delete a resource which is not web API enabled.
+    $this->enableService(FALSE);
     $account = $this->drupalCreateUser();
     // Reset cURL here because it is confused from our previously used cURL
     // options.
@@ -88,32 +78,7 @@ public function testDelete() {
     $this->drupalLogin($account);
     $this->httpRequest('entity/user/' . $account->id(), 'DELETE');
     $user = entity_load('user', $account->id(), TRUE);
-    $this->assertEqual($account->id(), $user->id());
+    $this->assertEqual($account->id(), $user->id(), 'User still exists in the database.');
     $this->assertResponse(404);
   }
-
-  /**
-   * Creates entity objects based on their types.
-   *
-   * Required properties differ from entity type to entity type, so we keep a
-   * minimum mapping here.
-   *
-   * @param string $entity_type
-   *   The type of the entity that should be created..
-   *
-   * @return \Drupal\Core\Entity\EntityInterface
-   *   The new entity object.
-   */
-  protected function entityCreate($entity_type) {
-    switch ($entity_type) {
-      case 'entity_test':
-        return entity_create('entity_test', array('name' => 'test', 'user_id' => 1));
-      case 'node':
-        return entity_create('node', array('title' => $this->randomString()));
-      case 'user':
-        return entity_create('user', array('name' => $this->randomName()));
-      default:
-        return entity_create($entity_type, array());
-    }
-  }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
index 3bc8756..eed04fb 100644
--- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
+++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php
@@ -31,13 +31,14 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
       case 'GET':
         // Set query if there are additional GET parameters.
         $options = isset($body) ? array('absolute' => TRUE, 'query' => $body) : array('absolute' => TRUE);
-        return $this->curlExec(array(
+        $response = $this->curlExec(array(
           CURLOPT_HTTPGET => TRUE,
           CURLOPT_URL => url($url, $options),
           CURLOPT_NOBODY => FALSE)
         );
+        break;
       case 'POST':
-        return $this->curlExec(array(
+        $response = $this->curlExec(array(
           CURLOPT_HTTPGET => FALSE,
           CURLOPT_POST => TRUE,
           CURLOPT_POSTFIELDS => $body,
@@ -45,8 +46,9 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
           CURLOPT_NOBODY => FALSE,
           CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
         ));
+        break;
       case 'PUT':
-        return $this->curlExec(array(
+        $response = $this->curlExec(array(
           CURLOPT_HTTPGET => FALSE,
           CURLOPT_CUSTOMREQUEST => 'PUT',
           CURLOPT_POSTFIELDS => $body,
@@ -54,13 +56,73 @@ protected function httpRequest($url, $method, $body = NULL, $format = 'applicati
           CURLOPT_NOBODY => FALSE,
           CURLOPT_HTTPHEADER => array('Content-Type: ' . $format),
         ));
+        break;
       case 'DELETE':
-        return $this->curlExec(array(
+        $response = $this->curlExec(array(
           CURLOPT_HTTPGET => FALSE,
           CURLOPT_CUSTOMREQUEST => 'DELETE',
           CURLOPT_URL => url($url, array('absolute' => TRUE)),
           CURLOPT_NOBODY => FALSE,
         ));
+        break;
     }
+
+    $this->verbose($method . ' request to: ' . $url .
+      '<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
+      '<hr />Response: ' . $response);
+
+    return $response;
+  }
+
+  /**
+   * Creates entity objects based on their types.
+   *
+   * Required properties differ from entity type to entity type, so we keep a
+   * minimum mapping here.
+   *
+   * @param string $entity_type
+   *   The type of the entity that should be created..
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The new entity object.
+   */
+  protected function entityCreate($entity_type) {
+    switch ($entity_type) {
+      case 'entity_test':
+        return entity_create('entity_test', array('name' => $this->randomName(), 'user_id' => 1));
+      case 'node':
+        return entity_create('node', array('title' => $this->randomString()));
+      case 'user':
+        return entity_create('user', array('name' => $this->randomName()));
+      default:
+        return entity_create($entity_type, array());
+    }
+  }
+
+  /**
+   * Enables the web service interface for a specific entity type.
+   *
+   * @param string|FALSE $resource_type
+   *   The resource type that should get web API enabled or FALSE to disable all
+   *   resource types.
+   */
+  protected function enableService($resource_type) {
+    // Enable web API for this entity type.
+    $config = config('rest');
+    if ($resource_type) {
+      $config->set('resources', array(
+        $resource_type => $resource_type,
+      ));
+    }
+    else {
+      $config->set('resources', array());
+    }
+    $config->save();
+
+    // Rebuild routing cache, so that the web API paths are available.
+    drupal_container()->get('router.builder')->rebuild();
+    // Reset the Simpletest permission cache, so that the new resource
+    // permissions get picked up.
+    drupal_static_reset('checkPermissions');
   }
 }
diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
new file mode 100644
index 0000000..37a9e3a
--- /dev/null
+++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\rest\test\ReadTest.
+ */
+
+namespace Drupal\rest\Tests;
+
+use Drupal\rest\Tests\RESTTestBase;
+
+/**
+ * Tests resource read operations on test entities, nodes and users.
+ */
+class ReadTest extends RESTTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('jsonld', 'rest', 'entity_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Read resource',
+      'description' => 'Tests the retrieval of resources.',
+      'group' => 'REST',
+    );
+  }
+
+  /**
+   * Tests several valid and invalid read requests on all entity types.
+   */
+  public function testRead() {
+    // @todo once EntityNG is implemented for other entity types use the full
+    // entity_get_info() for all entity types here.
+    $entity_test_info = entity_get_info('entity_test');
+    $entity_info = array('entity_test' => $entity_test_info);
+    foreach ($entity_info as $entity_type => $info) {
+      $this->enableService('entity:' . $entity_type);
+      // Create a user account that has the required permissions to delete
+      // resources via the web API.
+      $account = $this->drupalCreateUser(array('restful get entity:' . $entity_type));
+      // Reset cURL here because it is confused from our previously used cURL
+      // options.
+      unset($this->curlHandle);
+      $this->drupalLogin($account);
+
+      // Create an entity programmatically.
+      $entity = $this->entityCreate($entity_type);
+      $entity->save();
+      // Read it over the web API.
+      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/ld+json');
+      $this->assertResponse('200', 'HTTP response code is correct.');
+      $data = drupal_json_decode($response);
+      // Only assert one example property here, other properties should be
+      // checked in serialization tests.
+      $this->assertEqual($data['uuid'][LANGUAGE_DEFAULT][0]['value'], $entity->uuid(), 'Entity UUID is correct');
+
+      // Try to read an entity that does not exist.
+      $response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, 'application/ld+json');
+      $this->assertResponse(404);
+      $this->assertEqual($response, 'Entity with ID 9999 not found', 'Response message is correct.');
+
+      // Try to read an entity without proper permissions.
+      $this->drupalLogout();
+      $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/ld+json');
+      $this->assertResponse(403);
+      $this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
+    }
+    // Try to read a resource which is not web API enabled.
+    $account = $this->drupalCreateUser();
+    // Reset cURL here because it is confused from our previously used cURL
+    // options.
+    unset($this->curlHandle);
+    $this->drupalLogin($account);
+    $response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, 'application/ld+json');
+    $this->assertResponse(404);
+    $this->assertNull(drupal_json_decode($response), 'No valid JSON found.');
+  }
+}
diff --git a/core/modules/rest/rest.info b/core/modules/rest/rest.info
index 56c650c..c0bf4bd 100644
--- a/core/modules/rest/rest.info
+++ b/core/modules/rest/rest.info
@@ -3,4 +3,6 @@ description = Exposes entities and other resources as RESTful web API
 package = Core
 version = VERSION
 core = 8.x
+; @todo Remove this dependency once hard coding to JSON-LD is gone.
+dependencies[] = jsonld
 configure = admin/config/services/rest
diff --git a/core/modules/rest/rest.module b/core/modules/rest/rest.module
index a020845..b5594fe 100644
--- a/core/modules/rest/rest.module
+++ b/core/modules/rest/rest.module
@@ -5,9 +5,6 @@
  * RESTful web services module.
  */
 
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
 /**
  * Implements hook_menu().
  *
@@ -27,32 +24,6 @@ function rest_menu() {
 }
 
 /**
- * Implements hook_route_info().
- */
-function rest_route_info() {
-  $collection = new RouteCollection();
-
-  // This hook is called during module enabling where the manager has not been
-  // registered as service yet.
-  if (drupal_container()->has('plugin.manager.rest')) {
-    $manager = drupal_container()->get('plugin.manager.rest');
-    $resources = config('rest')->get('resources');
-    if ($resources && $enabled = array_intersect_key($manager->getDefinitions(), $resources)) {
-      foreach ($enabled as $key => $resource) {
-        $plugin = $manager->getInstance(array('id' => $key));
-
-        // @todo Switch to ->addCollection() once http://drupal.org/node/1819018 is resolved.
-        foreach ($plugin->routes() as $name => $route) {
-          $collection->add("rest.$name", $route);
-        }
-      }
-    }
-  }
-
-  return $collection;
-}
-
-/**
  * Implements hook_permission().
  */
 function rest_permission() {
diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
index 4137375..e461135 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php
@@ -86,4 +86,20 @@ public function testControllerPlaceholdersDefaultValues() {
     $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
   }
 
+  /**
+   * Checks that dynamically defined and altered routes work correctly.
+   *
+   * @see \Drupal\router_test\RouteSubscriber
+   */
+  public function testDynamicRoutes() {
+    // Test the dynamically added route.
+    $this->drupalGet('router_test/test5');
+    $this->assertResponse(200);
+    $this->assertRaw('test5', 'The correct string was returned because the route was successful.');
+
+    // Test the altered route.
+    $this->drupalGet('router_test/test6');
+    $this->assertResponse(200);
+    $this->assertRaw('test5', 'The correct string was returned because the route was successful.');
+  }
 }
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index d10d3e0..e9c0860 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -566,51 +566,6 @@ function hook_menu_get_item_alter(&$router_item, $path, $original_map) {
 }
 
 /**
- * Defines routes in the new router system.
- *
- * A route is a Symfony Route object.  See the Symfony documentation for more
- * details on the available options.  Of specific note:
- *  - _controller: This is the PHP callable that will handle a request matching
- *              the route.
- *  - _content: This is the PHP callable that will handle the body of a request
- *              matching this route.  A default controller will provide the page
- *              rendering around it.
- *
- * Typically you will only specify one or the other of those properties.
- *
- * @deprecated
- *   This mechanism for registering routes is temporary. It will be replaced
- *   by a more robust mechanism in the near future.  It is documented here
- *   only for completeness.
- */
-function hook_route_info() {
-  $collection = new RouteCollection();
-
-  $route = new Route('router_test/test1', array(
-    '_controller' => '\Drupal\router_test\TestControllers::test1'
-  ));
-  $collection->add('router_test_1', $route);
-
-  $route = new Route('router_test/test2', array(
-    '_content' => '\Drupal\router_test\TestControllers::test2'
-  ));
-  $collection->add('router_test_2', $route);
-
-  $route = new Route('router_test/test3/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test3'
-  ));
-  $collection->add('router_test_3', $route);
-
-  $route = new Route('router_test/test4/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test4',
-    'value' => 'narf',
-  ));
-  $collection->add('router_test_4', $route);
-
-  return $collection;
-}
-
-/**
  * Define menu items and page callbacks.
  *
  * This hook enables modules to register paths in order to define how URL
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php
new file mode 100644
index 0000000..f0b9bd7
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Definition of \Drupal\router_test\RouteTestSubscriber.
+ */
+
+namespace Drupal\router_test;
+
+use \Drupal\Core\Routing\RouteBuildEvent;
+use \Drupal\Core\Routing\RoutingEvents;
+use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use \Symfony\Component\Routing\Route;
+
+/**
+ * Listens to the dynamic route event and add a test route.
+ */
+class RouteTestSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Implements EventSubscriberInterface::getSubscribedEvents().
+   */
+  static function getSubscribedEvents() {
+    $events[RoutingEvents::DYNAMIC] = 'dynamicRoutes';
+    $events[RoutingEvents::ALTER] = 'alterRoutes';
+    return $events;
+  }
+
+  /**
+   * Adds a dynamic test route.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route building event.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The route collection that contains the new dynamic route.
+   */
+  public function dynamicRoutes(RouteBuildEvent $event) {
+    $collection = $event->getRouteCollection();
+    $route = new Route('/router_test/test5', array(
+      '_content' => '\Drupal\router_test\TestControllers::test5'
+    ));
+    $collection->add('router_test_5', $route);
+  }
+
+  /**
+   * Alters an existing test route.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route building event.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The altered route collection.
+   */
+  public function alterRoutes(RouteBuildEvent $event) {
+    if ($event->getModule() == 'router_test') {
+      $collection = $event->getRouteCollection();
+      $route = $collection->get('router_test_6');
+      // Change controller method from test1 to test5.
+      $route->setDefault('_controller', '\Drupal\router_test\TestControllers::test5');
+    }
+  }
+}
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php
new file mode 100644
index 0000000..f2e123b
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\router_test\RouterTestBundle.
+ */
+
+namespace Drupal\router_test;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Registers a dynamic route provider.
+ */
+class RouterTestBundle extends Bundle {
+
+  /**
+   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
+   */
+  public function build(ContainerBuilder $container) {
+    $container->register('router_test.subscriber', 'Drupal\router_test\RouteTestSubscriber')->addTag('event_subscriber');
+  }
+}
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php
index fa92fd8..bcf18b7 100644
--- a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php
@@ -30,4 +30,8 @@ public function test4($value) {
     return $value;
   }
 
+  public function test5() {
+    return "test5";
+  }
+
 }
diff --git a/core/modules/system/tests/modules/router_test/router_test.module b/core/modules/system/tests/modules/router_test/router_test.module
index 4da939d..b3d9bbc 100644
--- a/core/modules/system/tests/modules/router_test/router_test.module
+++ b/core/modules/system/tests/modules/router_test/router_test.module
@@ -1,34 +1 @@
 <?php
-
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * Implements hook_router_info().
- */
-function router_test_route_info() {
-  $collection = new RouteCollection();
-
-  $route = new Route('router_test/test1', array(
-    '_controller' => '\Drupal\router_test\TestControllers::test1'
-  ));
-  $collection->add('router_test_1', $route);
-
-  $route = new Route('router_test/test2', array(
-    '_content' => '\Drupal\router_test\TestControllers::test2'
-  ));
-  $collection->add('router_test_2', $route);
-
-  $route = new Route('router_test/test3/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test3'
-  ));
-  $collection->add('router_test_3', $route);
-
-  $route = new Route('router_test/test4/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test4',
-    'value' => 'narf',
-  ));
-  $collection->add('router_test_4', $route);
-
-  return $collection;
-}
diff --git a/core/modules/system/tests/modules/router_test/router_test.routing.yml b/core/modules/system/tests/modules/router_test/router_test.routing.yml
new file mode 100644
index 0000000..cc177d3
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/router_test.routing.yml
@@ -0,0 +1,25 @@
+router_test_1:
+  pattern: '/router_test/test1'
+  defaults:
+    _controller: '\Drupal\router_test\TestControllers::test1'
+
+router_test_2:
+  pattern: '/router_test/test2'
+  defaults:
+    _content: '\Drupal\router_test\TestControllers::test2'
+
+router_test_3:
+  pattern: '/router_test/test3/{value}'
+  defaults:
+    _content: '\Drupal\router_test\TestControllers::test3'
+
+router_test_4:
+  pattern: '/router_test/test4/{value}'
+  defaults:
+    _content: '\Drupal\router_test\TestControllers::test4'
+    value: 'narf'
+
+router_test_6:
+  pattern: '/router_test/test6'
+  defaults:
+    _controller: '\Drupal\router_test\TestControllers::test1'
diff --git a/core/update.php b/core/update.php
index 968e8f4..98296bb 100644
--- a/core/update.php
+++ b/core/update.php
@@ -459,7 +459,8 @@ function update_check_requirements($skip_warnings = FALSE) {
   ->addArgument(new Reference('database'));
 $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
   ->addArgument(new Reference('router.dumper'))
-  ->addArgument(new Reference('lock'));
+  ->addArgument(new Reference('lock'))
+  ->addArgument(new Reference('dispatcher'));
 
 // Turn error reporting back on. From now on, only fatal errors (which are
 // not passed through the error handler) will cause a message to be printed.
