diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php
new file mode 100644
index 0000000..396dbfb
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\taxonomy\TaxonomyRouteController.
+ */
+
+namespace Drupal\taxonomy;
+
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+/**
+ * Controller routines for taxonomy routes.
+ */
+class TaxonomyRouteController {
+
+  /**
+   * Page callback: Outputs JSON for taxonomy autocomplete suggestions.
+   *
+   * This callback outputs term name suggestions in response to Ajax requests
+   * made by the taxonomy autocomplete widget for taxonomy term reference
+   * fields. The output is a JSON object of plain-text term suggestions, keyed
+   * by the user-entered value with the completed term name appended.  Term
+   * names containing commas are wrapped in quotes.
+   *
+   * For example, suppose the user has entered the string 'red fish, blue' in
+   * the field, and there are two taxonomy terms, 'blue fish' and 'blue moon'.
+   * The JSON output would have the following structure:
+   * @code
+   *   {
+   *     "red fish, blue fish": "blue fish",
+   *     "red fish, blue moon": "blue moon",
+   *   };
+   * @endcode
+   *
+   * @param string $field_name
+   *   The name of the term reference field.
+   * @param string $tags_typed
+   *   (optional) A comma-separated list of term names entered in the
+   *   autocomplete form element. Only the last term is used for autocompletion.
+   *   Defaults to '' (an empty string).
+   *
+   * @see taxonomy_menu()
+   * @see taxonomy_field_widget_info()
+   */
+  public function autocomplete($field_name, $tags_typed = '') {
+    // If the request has a '/' in the search text, then the menu system
+    // will have split it into multiple arguments, recover the intended
+    // $tags_typed.
+    $args = func_get_args();
+    // Shift off the $field_name argument.
+    unset($args[0]);
+    $tags_typed = implode('/', $args);
+
+    // Make sure the field exists and is a taxonomy field.
+    if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') {
+      // Error string. The JavaScript handler will realize this is not JSON and
+      // will display it as debugging information.
+      return new Response(t('Taxonomy field @field_name not found.', array('@field_name' => $field_name)));
+    }
+
+    // The user enters a comma-separated list of tags. We only autocomplete
+    // the last tag.
+    $tags_typed = drupal_explode_tags($tags_typed);
+    $tag_last = drupal_strtolower(array_pop($tags_typed));
+
+    $matches = array();
+    if ($tag_last != '') {
+
+      // Part of the criteria for the query come from the field's own settings.
+      $vids = array();
+      $vocabularies = taxonomy_vocabulary_get_names();
+      foreach ($field['settings']['allowed_values'] as $tree) {
+        $vids[] = $vocabularies[$tree['vocabulary']]->vid;
+      }
+
+      $query = db_select('taxonomy_term_data', 't');
+      $query->addTag('translatable');
+      $query->addTag('term_access');
+
+      // Do not select already entered terms.
+      if (!empty($tags_typed)) {
+        $query->condition('t.name', $tags_typed, 'NOT IN');
+      }
+      // Select rows that match by term name.
+      $tags_return = $query
+        ->fields('t', array('tid', 'name'))
+        ->condition('t.vid', $vids)
+        ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE')
+        ->range(0, 10)
+        ->execute()
+        ->fetchAllKeyed();
+
+      $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
+
+      $term_matches = array();
+      foreach ($tags_return as $tid => $name) {
+        $n = $name;
+        // Term names containing commas or quotes must be wrapped in quotes.
+        if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
+          $n = '"' . str_replace('"', '""', $name) . '"';
+        }
+        $term_matches[$prefix . $n] = check_plain($name);
+      }
+    }
+
+    return new JsonResponse($term_matches);
+  }
+}
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index c28cc7b..f38de9c 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -10,6 +10,10 @@ use Drupal\taxonomy\Term;
 use Drupal\taxonomy\Vocabulary;
 use Drupal\Core\Entity\EntityInterface;
 
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+
 /**
  * Denotes that no term in the vocabulary has a parent.
  */
@@ -284,6 +288,18 @@ function taxonomy_theme() {
 }
 
 /**
+ * Implements hook_route_info().
+ */
+function taxonomy_route_info() {
+  $collection = new RouteCollection();
+
+  $route = new Route('taxonomy/autocomplete/{field_name}/{tags_typed}', array(
+    '_controller' => '\Drupal\taxonomy\TaxonomyRouteController::autocomplete',
+  ));
+  $collection->add('taxonomy_autocomplete', $route);
+  return $collection;
+}
+/**
  * Implements hook_menu().
  */
 function taxonomy_menu() {
@@ -353,14 +369,6 @@ function taxonomy_menu() {
     'type' => MENU_CALLBACK,
     'file' => 'taxonomy.pages.inc',
   );
-  $items['taxonomy/autocomplete'] = array(
-    'title' => 'Autocomplete taxonomy',
-    'page callback' => 'taxonomy_autocomplete',
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-    'file' => 'taxonomy.pages.inc',
-  );
-
   $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name'] = array(
     'title callback' => 'entity_page_label',
     'title arguments' => array(3),
diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc
index 7c77716..082ba29 100644
--- a/core/modules/taxonomy/taxonomy.pages.inc
+++ b/core/modules/taxonomy/taxonomy.pages.inc
@@ -81,94 +81,3 @@ function taxonomy_term_feed(Term $term) {
   node_feed($nids, $channel);
 }
 
-/**
- * Page callback: Outputs JSON for taxonomy autocomplete suggestions.
- *
- * This callback outputs term name suggestions in response to Ajax requests
- * made by the taxonomy autocomplete widget for taxonomy term reference
- * fields. The output is a JSON object of plain-text term suggestions, keyed by
- * the user-entered value with the completed term name appended.  Term names
- * containing commas are wrapped in quotes.
- *
- * For example, suppose the user has entered the string 'red fish, blue' in the
- * field, and there are two taxonomy terms, 'blue fish' and 'blue moon'. The
- * JSON output would have the following structure:
- * @code
- *   {
- *     "red fish, blue fish": "blue fish",
- *     "red fish, blue moon": "blue moon",
- *   };
- * @endcode
- *
- * @param $field_name
- *   The name of the term reference field.
- * @param $tags_typed
- *   (optional) A comma-separated list of term names entered in the
- *   autocomplete form element. Only the last term is used for autocompletion.
- *   Defaults to '' (an empty string).
- *
- * @see taxonomy_menu()
- * @see taxonomy_field_widget_info()
- */
-function taxonomy_autocomplete($field_name, $tags_typed = '') {
-  // If the request has a '/' in the search text, then the menu system will have
-  // split it into multiple arguments, recover the intended $tags_typed.
-  $args = func_get_args();
-  // Shift off the $field_name argument.
-  array_shift($args);
-  $tags_typed = implode('/', $args);
-
-  // Make sure the field exists and is a taxonomy field.
-  if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') {
-    // Error string. The JavaScript handler will realize this is not JSON and
-    // will display it as debugging information.
-    print t('Taxonomy field @field_name not found.', array('@field_name' => $field_name));
-    exit;
-  }
-
-  // The user enters a comma-separated list of tags. We only autocomplete the last tag.
-  $tags_typed = drupal_explode_tags($tags_typed);
-  $tag_last = drupal_strtolower(array_pop($tags_typed));
-
-  $matches = array();
-  if ($tag_last != '') {
-
-    // Part of the criteria for the query come from the field's own settings.
-    $vids = array();
-    $vocabularies = taxonomy_vocabulary_get_names();
-    foreach ($field['settings']['allowed_values'] as $tree) {
-      $vids[] = $vocabularies[$tree['vocabulary']]->vid;
-    }
-
-    $query = db_select('taxonomy_term_data', 't');
-    $query->addTag('translatable');
-    $query->addTag('term_access');
-
-    // Do not select already entered terms.
-    if (!empty($tags_typed)) {
-      $query->condition('t.name', $tags_typed, 'NOT IN');
-    }
-    // Select rows that match by term name.
-    $tags_return = $query
-      ->fields('t', array('tid', 'name'))
-      ->condition('t.vid', $vids)
-      ->condition('t.name', '%' . db_like($tag_last) . '%', 'LIKE')
-      ->range(0, 10)
-      ->execute()
-      ->fetchAllKeyed();
-
-    $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : '';
-
-    $term_matches = array();
-    foreach ($tags_return as $tid => $name) {
-      $n = $name;
-      // Term names containing commas or quotes must be wrapped in quotes.
-      if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
-        $n = '"' . str_replace('"', '""', $name) . '"';
-      }
-      $term_matches[$prefix . $n] = check_plain($name);
-    }
-  }
-
-  return new JsonResponse($term_matches);
-}
