diff --git a/core/modules/search/lib/Drupal/search/Access/SearchCheck.php b/core/modules/search/lib/Drupal/search/Access/SearchCheck.php
new file mode 100644
index 0000000..702d3ef
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Access/SearchCheck.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Access\SearchCheck.
+ */
+
+namespace Drupal\search\Access;
+
+use Drupal\Core\Access\AccessCheckInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Checks access for viewing search.
+ */
+class SearchCheck implements AccessCheckInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return array_key_exists('_search_view_access', $route->getRequirements());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    return user_access('search content') && search_get_info();
+  }
+
+}
diff --git a/core/modules/search/lib/Drupal/search/Controller/SearchController.php b/core/modules/search/lib/Drupal/search/Controller/SearchController.php
new file mode 100644
index 0000000..13f3240
--- /dev/null
+++ b/core/modules/search/lib/Drupal/search/Controller/SearchController.php
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\search\Controller\SearchController.
+ */
+
+namespace Drupal\search\Controller;
+
+use Drupal\Core\ControllerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Returns responses for search module routes.
+ */
+class SearchController implements ControllerInterface {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * Creates a new SearchController.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('module_handler')
+    );
+  }
+
+  /**
+   * Presents the search form and/or search results.
+   *
+   * @param string $module
+   *   Search module to use for the search.
+   * @param string $keys
+   *   Keywords to use for the search.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return array
+   *   An array as expected by drupal_render().
+   */
+  public function searchView($module, $keys, Request $request) {
+    $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.
+    $request_keys = $request->request->get('keys');
+    if (!$keys && !empty($request_keys)) {
+      $keys = trim($request_keys);
+    }
+
+    if (!empty($module)) {
+      $active_module_info = search_get_info();
+      if (isset($active_module_info[$module])) {
+        $info = $active_module_info[$module];
+      }
+    }
+
+    if (empty($info)) {
+      // No path or invalid path: find the default module. Note that if there
+      // are no enabled search modules, this function should never be called,
+      // since hook_menu() would not have defined any search paths.
+      $info = search_get_default_module_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($path);
+    }
+
+    // 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/[module 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.
+    $form_id = $request->request->get('form_id');
+    if (empty($form_id) || $form_id != 'search_form') {
+      $conditions = NULL;
+      if (isset($info['conditions_callback'])) {
+        // Build an optional array of more search conditions.
+        $conditions = call_user_func($info['conditions_callback'], $keys);
+      }
+      // Only search if there are keywords or non-empty conditions.
+      if ($keys || !empty($conditions)) {
+        // 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 = search_data($keys, $info['module'], $conditions);
+      }
+    }
+
+    // @todo Refactor this once search form is converted to form interface.
+    $this->moduleHandler->loadInclude('search', 'inc', 'search.pages');
+
+    // The form may be altered based on whether the search was run.
+    $build['search_form'] = drupal_get_form('search_form', NULL, $keys, $info['module']);
+    $build['search_results'] = $results;
+
+    return $build;
+  }
+}
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index f31488b..d6de54b 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -150,10 +150,8 @@ function search_preprocess_block(&$variables) {
 function search_menu() {
   $items['search'] = array(
     'title' => 'Search',
-    'page callback' => 'search_view',
-    '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',
diff --git a/core/modules/search/search.pages.inc b/core/modules/search/search.pages.inc
index eaae71a..d9455b6 100644
--- a/core/modules/search/search.pages.inc
+++ b/core/modules/search/search.pages.inc
@@ -6,72 +6,6 @@
  */
 
 /**
- * Page callback: Presents the search form and/or search results.
- *
- * @param $module
- *   Search module to use for the search.
- * @param $keys
- *   Keywords to use for the search.
- */
-function search_view($module = 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']);
-  }
-
-  if (!empty($module)) {
-    $active_module_info = search_get_info();
-    if (isset($active_module_info[$module])) {
-      $info = $active_module_info[$module];
-    }
-  }
-
-  if (empty($info)) {
-    // No path or invalid path: find the default module. Note that if there
-    // are no enabled search modules, this function should never be called,
-    // since hook_menu() would not have defined any search paths.
-    $info = search_get_default_module_info();
-    // Redirect from bare /search or an invalid path to the default search path.
-    $path = 'search/' . $info['path'];
-    if ($keys) {
-      $path .= '/' . $keys;
-    }
-    drupal_goto($path);
-  }
-
-  // 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/[module 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') {
-    $conditions =  NULL;
-    if (isset($info['conditions_callback'])) {
-      // Build an optional array of more search conditions.
-      $conditions = call_user_func($info['conditions_callback'], $keys);
-    }
-    // Only search if there are keywords or non-empty conditions.
-    if ($keys || !empty($conditions)) {
-      // 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 = search_data($keys, $info['module'], $conditions);
-    }
-  }
-  // The form may be altered based on whether the search was run.
-  $build['search_form'] = drupal_get_form('search_form', NULL, $keys, $info['module']);
-  $build['search_results'] = $results;
-
-  return $build;
-}
-
-/**
  * Prepares variables for search results templates.
  *
  * Default template: search-results.html.twig.
diff --git a/core/modules/search/search.routing.yml b/core/modules/search/search.routing.yml
index 5521e18..1df5873 100644
--- a/core/modules/search/search.routing.yml
+++ b/core/modules/search/search.routing.yml
@@ -4,3 +4,13 @@ search_reindex_confirm:
     _form: 'Drupal\search\Form\ReindexConfirm'
   requirements:
     _permission: 'administer search'
+
+search_view:
+  pattern: '/search/{module}/{keys}'
+  method: [GET,POST]
+  defaults:
+    _content: 'Drupal\search\Controller\SearchController::searchView'
+    module: ~
+    keys: ''
+  requirements:
+    _search_view_access: 'TRUE'
diff --git a/core/modules/search/search.services.yml b/core/modules/search/search.services.yml
new file mode 100644
index 0000000..66a03df
--- /dev/null
+++ b/core/modules/search/search.services.yml
@@ -0,0 +1,5 @@
+services:
+  access_check.search_view:
+    class: Drupal\search\Access\SearchCheck
+    tags:
+      - { name: access_check }
