diff --git c/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php w/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php
new file mode 100644
index 0000000..d607fd7
--- /dev/null
+++ w/core/modules/search/lib/Drupal/search/Access/SearchAccessCheck.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search\Access\SearchAccessCheck
+ */
+
+namespace Drupal\search\Access;
+
+use Drupal\Core\Access\StaticAccessCheckInterface;
+use Drupal\search\SearchPluginManager;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Checks access for viewing search.
+ */
+class SearchAccessCheck implements StaticAccessCheckInterface {
+
+  /**
+   * The search plugin manager.
+   *
+   * @var \Drupal\search\SearchPluginManager
+   */
+  protected $searchManager;
+
+  /**
+   * Contructs a new search access check.
+   *
+   * @param SearchPluginManager $search_plugin_manager
+   *   The search plugin manager.
+   */
+  public function __construct(SearchPluginManager $search_plugin_manager) {
+    $this->searchManager = $search_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return array_key_exists('search_content', $route->getRequirements());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    return $this->searchManager->getActiveDefinitions() ? static::ALLOW : static::DENY;
+  }
+
+}
diff --git c/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php w/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php
new file mode 100644
index 0000000..f82e52e
--- /dev/null
+++ w/core/modules/search/lib/Drupal/search/Access/SearchPluginAccessCheck.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search\Access\SearchPluginAccessCheck
+ */
+
+namespace Drupal\search\Access;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Route access check for search plugins.
+ */
+class SearchPluginAccessCheck extends SearchAccessCheck {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    $account = $request->attributes->get('_account');
+    $requirements = $route->getRequirements();
+    $plugin_id = $requirements['_search_plugin_view_access'];
+    $access = !empty($account) && $account->hasPermission('search content');
+    return $access && $this->searchManager->pluginAccess($plugin_id, $account);
+  }
+}
diff --git c/core/modules/search/lib/Drupal/search/Controller/SearchController.php w/core/modules/search/lib/Drupal/search/Controller/SearchController.php
new file mode 100644
index 0000000..8a6b0fd
--- /dev/null
+++ w/core/modules/search/lib/Drupal/search/Controller/SearchController.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search\Controller\SearchController
+ */
+
+namespace Drupal\search\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\search\SearchPluginManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Route controller for search.
+ */
+class SearchController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * The search plugin manager.
+   *
+   * @var \Drupal\search\SearchPluginManager
+   */
+  protected $searchManager;
+
+  /**
+   * Constructs a new search controller.
+   *
+   * @param \Drupal\search\SearchPluginManager $search_plugin_manager
+   *   The search plugin manager.
+   */
+  public function __construct(SearchPluginManager $search_plugin_manager) {
+    $this->searchManager = $search_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('plugin.manager.search'));
+  }
+
+  /**
+   * @param Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   * @param string $plugin_id
+   *   The id of a plugin, i.e. the data type.
+   * @param string $keys
+   *   search keywords.
+   *
+   * @return array
+   *   The search form and search results.
+   *
+   */
+  public function view(Request $request, $plugin_id = NULL, $keys = NULL) {
+    $info = FALSE;
+    $keys = trim($keys);
+    // Also try to pull search keywords out of the $_REQUEST variable to
+    // support old GET format of searches for existing links.
+    if (!$keys && $request->query->has('keys')) {
+      $keys = trim($request->query->get('keys'));
+    }
+
+    if (!empty($plugin_id)) {
+      $active_plugin_info = $this->searchManager->getActiveDefinitions();
+      if (isset($active_plugin_info[$plugin_id])) {
+        $info = $active_plugin_info[$plugin_id];
+      }
+    }
+
+    if (empty($plugin_id) || empty($info)) {
+      // No path or invalid path: find the default plugin. Note that if there
+      // are no enabled search plugins, this function should never be called,
+      // since hook_menu() would not have defined any search paths.
+      $info = search_get_default_plugin_info();
+      // Redirect from bare /search or an invalid path to the default search
+      // path.
+      $path = 'search/' . $info['path'];
+      if ($keys) {
+        $path .= '/' . $keys;
+      }
+      return $this->redirect($this->getUrlGenerator()->generateFromPath($path, array('absolute' => TRUE)));
+    }
+    $plugin = $this->searchManager->createInstance($plugin_id);
+    $plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
+    // Default results output is an empty string.
+    $results = array('#markup' => '');
+
+    // Process the search form. Note that if there is $_POST data,
+    // search_form_submit() will cause a redirect to search/[path]/[keys],
+    // which will get us back to this page callback. In other words, the search
+    // form submits with POST but redirects to GET. This way we can keep
+    // the search query URL clean as a whistle.
+    if ($request->request->has('form_id') || $request->request->get('form_id') != 'search_form') {
+      // Only search if there are keywords or non-empty conditions.
+      if ($plugin->isSearchExecutable()) {
+        // Log the search keys.
+        watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys));
+
+        // Collect the search results.
+        $results = $plugin->buildResults();
+      }
+    }
+    // The form may be altered based on whether the search was run.
+    $build['search_form'] = drupal_get_form(SearchForm::create($this->container), $plugin_id);
+    $build['search_results'] = $results;
+    return $build;
+  }
+
+}
diff --git c/core/modules/search/lib/Drupal/search/Form/SearchForm.php w/core/modules/search/lib/Drupal/search/Form/SearchForm.php
new file mode 100644
index 0000000..090e044
--- /dev/null
+++ w/core/modules/search/lib/Drupal/search/Form/SearchForm.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\search\Form\SearchForm.
+ */
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\search\SearchPluginManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * The Search Form.
+ */
+class SearchForm extends FormBase {
+
+  /**
+   * The search plugin manager.
+   *
+   * @var \Drupal\search\SearchPluginManager
+   */
+  protected $searchManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.search')
+    );
+  }
+
+  /**
+   * Constructs a search form.
+   *
+   * @param Drupal\search\SearchPluginManager
+   *   The search plugin manager.
+   *
+   */
+  public function __construct(SearchInterface $search_plugin) {
+    $this->searchManager = $search_plugin;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'search_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, $plugin_id = NULL) {
+    $plugin = $this->searchManager->createInstance($plugin_id);
+    $plugin_info = $plugin->getPluginDefinition();
+
+    if (!$action) {
+      $action = 'search/' . $plugin_info['path'];
+    }
+    if (!isset($prompt)) {
+      $prompt = $this->t('Enter your keywords');
+    }
+
+    $form['#action'] = $this->getUrlGenerator()->generateFromPath($action);
+    // Record the $action for later use in redirecting.
+    $form_state['action'] = $action;
+    $form['plugin_id'] = array(
+      '#type' => 'value',
+      '#value' => $plugin->getPluginId(),
+    );
+    $form['basic'] = array(
+      '#type' => 'container',
+      '#attributes' => array(
+        'class' => array('container-inline'),
+      ),
+    );
+    $form['basic']['keys'] = array(
+      '#type' => 'search',
+      '#title' => $prompt,
+      '#default_value' => $plugin->getKeywords(),
+      '#size' => $prompt ? 40 : 20,
+      '#maxlength' => 255,
+    );
+    // processed_keys is used to coordinate keyword passing between other forms
+    // that hook into the basic search form.
+    $form['basic']['processed_keys'] = array(
+      '#type' => 'value',
+      '#value' => '',
+    );
+    $form['basic']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Search'),
+    );
+    // Allow the plugin to add to or alter the search form.
+    $plugin->searchFormAlter($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array $form, array &$form_state) {
+    form_set_value($form['basic']['processed_keys'], trim($form_state['values']['keys']), $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array $form, array &$form_state) {
+    $keys = $form_state['values']['processed_keys'];
+    if ($keys == '') {
+      form_set_error('keys', t('Please enter some keywords.'));
+      // Fall through to the form redirect.
+    }
+
+    $form_state['redirect'] = $form_state['action'] . '/' . $keys;
+  }
+}
diff --git c/core/modules/search/lib/Drupal/search/Routing/SearchRouteSubscriber.php w/core/modules/search/lib/Drupal/search/Routing/SearchRouteSubscriber.php
new file mode 100644
index 0000000..ada6075
--- /dev/null
+++ w/core/modules/search/lib/Drupal/search/Routing/SearchRouteSubscriber.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\search\Routing\SearchRouteSubscriber
+ */
+
+namespace Drupal\search\Routing;
+
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Drupal\search\SearchPluginManager;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides dynamic routes for search.
+ */
+class SearchRouteSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The search plugin manager.
+   *
+   * @var \Drupal\search\SearchPluginManager
+   */
+  protected $searchPluginManager;
+
+  /**
+   * Constructs a new search route subscriber.
+   *
+   * @param \Drupal\search\SearchPluginManager $search_plugin_manager
+   *   The search plugin manager.
+   */
+  public function __construct(SearchPluginManager $search_plugin_manager) {
+    $this->searchPluginManager = $search_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[RoutingEvents::DYNAMIC] = 'routes';
+    return $events;
+  }
+
+  /**
+   * Adds routes for search.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route building event.
+   */
+  public function routes(RouteBuildEvent $event) {
+    $collection = $event->getRouteCollection();
+
+    $default_info = search_get_default_plugin_info();
+    if ($default_info) {
+      foreach ($this->searchPluginManager->getActiveDefinitions() as $plugin_id => $search_info) {
+        $route = new Route(
+          'search/' . $search_info['path'],
+          array(
+            '_content' => 'Drupal\search\Controller\SearchController::searchView',
+            'plugin_id' => $plugin_id,
+            'keys' => '',
+          ),
+          array('_search_plugin_view_access' => $plugin_id)
+        );
+        $collection->add('search_view.' . $plugin_id, $route);
+      }
+    }
+  }
+}
diff --git c/core/modules/search/search.module w/core/modules/search/search.module
index a4830ad..5b7e6de 100644
--- c/core/modules/search/search.module
+++ w/core/modules/search/search.module
@@ -152,11 +152,8 @@ function search_preprocess_block(&$variables) {
 function search_menu() {
   $items['search'] = array(
     'title' => 'Search',
-    'page callback' => 'search_view',
-    'page arguments' => array(NULL, '', ''),
-    'access callback' => 'search_is_active',
     'type' => MENU_SUGGESTED_ITEM,
-    'file' => 'search.pages.inc',
+    'route_name' => 'search_view',
   );
   $items['admin/config/search/settings'] = array(
     'title' => 'Search settings',
@@ -183,10 +180,7 @@ function search_menu() {
       $path = 'search/' . $search_info['path'];
       $items[$path] = array(
         'title' => $search_info['title'],
-        'page callback' => 'search_view',
-        'page arguments' => array($plugin_id, ''),
-        'access callback' => '_search_menu_access',
-        'access arguments' => array($plugin_id),
+        'route_name' => 'search_view',
         'type' => MENU_LOCAL_TASK,
         'file' => 'search.pages.inc',
         'weight' => $plugin_id == $default_info['id'] ? -10 : 0,
@@ -214,15 +208,6 @@ function search_menu() {
 }
 
 /**
- * Determines access for the 'search' path.
- */
-function search_is_active() {
-  // This path cannot be accessed if there are no active plugins.
-  $account = Drupal::request()->attributes->get('_account');
-  return !empty($account) && $account->hasPermission('search content') && Drupal::service('plugin.manager.search')->getActiveDefinitions();
-}
-
-/**
  * Returns information about the default search plugin.
  *
  * @return array
@@ -240,26 +225,6 @@ function search_get_default_plugin_info() {
 }
 
 /**
- * Access callback: Determines access for a search page.
- *
- * @param string $plugin_id
- *   The name of a search plugin (e.g., 'node_search').
- *
- * @return bool
- *   TRUE if a user has access to the search page; FALSE otherwise.
- *
- * @see search_menu()
- */
-function _search_menu_access($plugin_id) {
-  $account = Drupal::request()->attributes->get('_account');
-  // @todo - remove the empty() check once we are more confident
-  // that the account will be populated, especially during tests.
-  // @see https://drupal.org/node/2032553
-  $access = !empty($account) && $account->hasPermission('search content');
-  return $access && Drupal::service('plugin.manager.search')->pluginAccess($plugin_id, $account);
-}
-
-/**
  * Clears either a part of, or the entire search index.
  *
  * @param $sid
@@ -693,60 +658,6 @@ function search_mark_for_reindex($type, $sid) {
  */
 
 /**
- * Form constructor for the search form.
- *
- * @param \Drupal\search\Plugin\SearchInterface $plugin
- *   A search plugin instance to render the form for.
- * @param $action
- *   Form action. Defaults to "search/$path", where $path is the search path
- *   associated with the plugin in its definition. This will be run through
- *   url().
- * @param $prompt
- *   Label for the keywords field. Defaults to t('Enter your keywords') if
- *   NULL. Supply '' to omit.
- *
- * @see search_form_validate()
- * @see search_form_submit()
- *
- * @ingroup forms
- */
-function search_form($form, &$form_state, SearchInterface $plugin, $action = '', $prompt = NULL) {
-
-  $plugin_info = $plugin->getPluginDefinition();
-
-  if (!$action) {
-    $action = 'search/' . $plugin_info['path'];
-  }
-  if (!isset($prompt)) {
-    $prompt = t('Enter your keywords');
-  }
-
-  $form['#action'] = url($action);
-  // Record the $action for later use in redirecting.
-  $form_state['action'] = $action;
-  $form['plugin_id'] = array('#type' => 'value', '#value' => $plugin->getPluginId());
-  $form['basic'] = array('#type' => 'container', '#attributes' => array('class' => array('container-inline')));
-  $form['basic']['keys'] = array(
-    '#type' => 'search',
-    '#title' => $prompt,
-    '#default_value' => $plugin->getKeywords(),
-    '#size' => $prompt ? 40 : 20,
-    '#maxlength' => 255,
-  );
-  // processed_keys is used to coordinate keyword passing between other forms
-  // that hook into the basic search form.
-  $form['basic']['processed_keys'] = array('#type' => 'value', '#value' => '');
-  $form['basic']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
-  // Make sure the default validate and submit handlers are added.
-  $form['#validate'][] = 'search_form_validate';
-  $form['#submit'][] = 'search_form_submit';
-  // Allow the plugin to add to or alter the search form.
-  $plugin->searchFormAlter($form, $form_state);
-
-  return $form;
-}
-
-/**
  * Form constructor for the search block's search box.
  *
  * @param $form_id
diff --git c/core/modules/search/search.pages.inc w/core/modules/search/search.pages.inc
index 804e4be..50df3cb 100644
--- c/core/modules/search/search.pages.inc
+++ w/core/modules/search/search.pages.inc
@@ -9,70 +9,6 @@
 use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
- * Page callback: Presents the search form and/or search results.
- *
- * @param $plugin_id
- *   Search plugin_id to use for the search.
- * @param $keys
- *   Keywords to use for the search.
- */
-function search_view($plugin_id = NULL, $keys = '') {
-  $info = FALSE;
-  $keys = trim($keys);
-  // Also try to pull search keywords out of the $_REQUEST variable to
-  // support old GET format of searches for existing links.
-  if (!$keys && !empty($_REQUEST['keys'])) {
-    $keys = trim($_REQUEST['keys']);
-  }
-
-  $manager = Drupal::service('plugin.manager.search');
-  if (!empty($plugin_id)) {
-    $active_plugin_info = $manager->getActiveDefinitions();
-    if (isset($active_plugin_info[$plugin_id])) {
-      $info = $active_plugin_info[$plugin_id];
-    }
-  }
-
-  if (empty($plugin_id) || empty($info)) {
-    // No path or invalid path: find the default plugin. Note that if there
-    // are no enabled search plugins, this function should never be called,
-    // since hook_menu() would not have defined any search paths.
-    $info = search_get_default_plugin_info();
-    // Redirect from bare /search or an invalid path to the default search path.
-    $path = 'search/' . $info['path'];
-    if ($keys) {
-      $path .= '/' . $keys;
-    }
-    return new RedirectResponse(url($path, array('absolute' => TRUE)));
-  }
-  $plugin = $manager->createInstance($plugin_id);
-  $request = Drupal::request();
-  $plugin->setSearch($keys, $request->query->all(), $request->attributes->all());
-  // Default results output is an empty string.
-  $results = array('#markup' => '');
-  // Process the search form. Note that if there is $_POST data,
-  // search_form_submit() will cause a redirect to search/[path]/[keys],
-  // which will get us back to this page callback. In other words, the search
-  // form submits with POST but redirects to GET. This way we can keep
-  // the search query URL clean as a whistle.
-  if (empty($_POST['form_id']) || $_POST['form_id'] != 'search_form') {
-    // Only search if there are keywords or non-empty conditions.
-    if ($plugin->isSearchExecutable()) {
-      // Log the search keys.
-      watchdog('search', 'Searched %type for %keys.', array('%keys' => $keys, '%type' => $info['title']), WATCHDOG_NOTICE, l(t('results'), 'search/' . $info['path'] . '/' . $keys));
-
-      // Collect the search results.
-      $results = $plugin->buildResults();
-    }
-  }
-  // The form may be altered based on whether the search was run.
-  $build['search_form'] = drupal_get_form('search_form', $plugin);
-  $build['search_results'] = $results;
-
-  return $build;
-}
-
-/**
  * Prepares variables for search results templates.
  *
  * Default template: search-results.html.twig.
@@ -149,31 +85,3 @@ function template_preprocess_search_result(&$variables) {
   $variables['theme_hook_suggestions'][] = 'search_result__' . $variables['plugin_id'];
 }
 
-/**
- * Form validation handler for search_form().
- *
- * As the search form collates keys from other modules hooked in via
- * hook_form_alter, the validation takes place in search_form_submit().
- * search_form_validate() is used solely to set the 'processed_keys' form
- * value for the basic search form.
- *
- * @see search_form_submit()
- */
-function search_form_validate($form, &$form_state) {
-  form_set_value($form['basic']['processed_keys'], trim($form_state['values']['keys']), $form_state);
-}
-
-/**
- * Form submission handler for search_form().
- *
- * @see search_form_validate()
- */
-function search_form_submit($form, &$form_state) {
-  $keys = $form_state['values']['processed_keys'];
-  if ($keys == '') {
-    form_set_error('keys', t('Please enter some keywords.'));
-    // Fall through to the form redirect.
-  }
-
-  $form_state['redirect'] = $form_state['action'] . '/' . $keys;
-}
diff --git c/core/modules/search/search.routing.yml w/core/modules/search/search.routing.yml
index 8acf650..3e76916 100644
--- c/core/modules/search/search.routing.yml
+++ w/core/modules/search/search.routing.yml
@@ -1,12 +1,23 @@
 search_settings:
   pattern: '/admin/config/search/settings'
   defaults:
-    _form: 'Drupal\search\Form\SearchSettingsForm'
+    _form: '\Drupal\search\Form\SearchSettingsForm'
   requirements:
     _permission: 'administer search'
+
 search_reindex_confirm:
   pattern: '/admin/config/search/settings/reindex'
   defaults:
-    _form: 'Drupal\search\Form\ReindexConfirm'
+    _form: '\Drupal\search\Form\ReindexConfirm'
   requirements:
     _permission: 'administer search'
+
+search_view:
+  pattern: '/search/{plugin_id}/{keys}'
+  defaults:
+    _content: '\Drupal\search\Controller\SearchController::view'
+    plugin_id: NULL
+    keys: ''
+  requirements:
+    _permission: 'search content'
+    _access_mode: 'ALL'
diff --git c/core/modules/search/search.services.yml w/core/modules/search/search.services.yml
index 22dc7f2..8af9864 100644
--- c/core/modules/search/search.services.yml
+++ w/core/modules/search/search.services.yml
@@ -2,3 +2,21 @@ services:
   plugin.manager.search:
     class: Drupal\search\SearchPluginManager
     arguments: ['@container.namespaces', '@config.factory']
+
+  access_check.search:
+    class: Drupal\search\Access\SearchAccessCheck
+    arguments: ['@plugin.manager.search']
+    tags:
+      - { name: access_check }
+
+  access_check.search_plugin:
+    class: Drupal\search\Access\SearchPluginAccessCheck
+    arguments: ['@plugin.manager.search']
+    tags:
+      - { name: access_check }
+
+  route_subscriber.search:
+    class: Drupal\search\Routing\SearchRouteSubscriber
+    arguments: ['@plugin.manager.search']
+    tags:
+     - { name: event_subscriber }
