diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php
new file mode 100644
index 0000000..96e124c
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\Controller\TermAutocompleteController.
+ */
+
+namespace Drupal\taxonomy\Controller;
+
+use Drupal\Component\Utility\Tags;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\String;
+use Drupal\Core\Controller\ControllerInterface;
+use Drupal\Core\Entity\Query\QueryInterface;
+use Drupal\field\FieldInfo;
+use Drupal\taxonomy\TermStorageControllerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+/**
+ * Returns responses for Views UI routes.
+ */
+class TermAutocompleteController implements ControllerInterface {
+
+  /**
+   * Taxonomy term entity query interface.
+   *
+   * @var \Drupal\Core\Entity\Query\QueryInterface
+   */
+  protected $termEntityQuery;
+
+  /**
+   * Field info service.
+   *
+   * @var \Drupal\field\FieldInfo
+   */
+  protected $fieldInfo;
+
+  /**
+   * Term storage controller.
+   *
+   * @var \Drupal\taxonomy\TermStorageControllerInterface
+   */
+  protected $termStorageController;
+
+  /**
+   * Constructs a new \Drupal\taxonomy\Controller\TermAutocompleteController object.
+   *
+   * @param \Drupal\Core\Entity\Query\QueryInterface $entityQuery
+   *   The entity query service.
+   * @param \Drupal\field\FieldInfo $fieldInfo
+   *   The field info service.
+   * @param \Drupal\taxonomy\TermStorageControllerInterface $termStorageController
+   *   The term storage controller.
+   */
+  public function __construct(QueryInterface $termEntityQuery, FieldInfo $fieldInfo, TermStorageControllerInterface $termStorageController) {
+    $this->termEntityQuery = $termEntityQuery;
+    $this->fieldInfo = $fieldInfo;
+    $this->termStorageController = $termStorageController;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.query')->get('taxonomy_term'),
+      $container->get('field.info'),
+      $container->get('plugin.manager.entity')->getStorageController('taxonomy_term')
+    );
+  }
+
+  /**
+   * Retrieves suggestions for taxonomy term autocompletion.
+   *
+   * This function 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 \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   *
+   * @param string $field_name
+   *   The name of the term reference field.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse | \Symfony\Component\HttpFoundation\Response
+   *   When valid field name is specified, a JSON response containing the
+   *   autocomplete suggestions for taxonomy terms. Otherwie a normal response
+   *   containing an error message.
+   */
+  public function autocomplete(Request $request, $field_name) {
+    // A comma-separated list of term names entered in the autocomplete form
+    // element. Only the last term is used for autocompletion.
+    $tags_typed = $request->query->get('q');
+
+    // Make sure the field exists and is a taxonomy field.
+    if (!($field = $this->fieldInfo->getField($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)), 403);
+    }
+
+    // The user enters a comma-separated list of tags. We only autocomplete the
+    // last tag.
+    $tags_typed = Tags::explode($tags_typed);
+    $tag_last = Unicode::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();
+      foreach ($field['settings']['allowed_values'] as $tree) {
+        $vids[] = $tree['vocabulary'];
+      }
+
+      $this->termEntityQuery->addTag('term_access');
+
+      // Do not select already entered terms.
+      if (!empty($tags_typed)) {
+        $this->termEntityQuery->condition('name', $tags_typed, 'NOT IN');
+      }
+      // Select rows that match by term name.
+      $tids = $this->termEntityQuery
+        ->condition('vid', $vids, 'IN')
+        ->condition('name', $tag_last, 'CONTAINS')
+        ->range(0, 10)
+        ->execute();
+
+      $prefix = count($tags_typed) ? Tags::implode($tags_typed) . ', ' : '';
+      if (!empty($tids)) {
+        $terms = $this->termStorageController->loadMultiple(array_keys($tids));
+        foreach ($terms as $term) {
+          $name = $term->label();
+          // Term names containing commas or quotes must be wrapped in quotes.
+          if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
+            $name = '"' . str_replace('"', '""', $name) . '"';
+          }
+          $matches[$prefix . $name] = String::checkPlain($term->label());
+        }
+      }
+    }
+
+    return new JsonResponse($matches);
+  }
+
+}
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 17bbb8c..2bd7a70 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -292,14 +292,6 @@ function taxonomy_menu() {
     'type' => MENU_CALLBACK,
     'file' => 'taxonomy.pages.inc',
   );
-  $items['taxonomy/autocomplete/%'] = array(
-    'title' => 'Autocomplete taxonomy',
-    'page callback' => 'taxonomy_autocomplete',
-    'page arguments' => array(2),
-    'access arguments' => array('access content'),
-    'type' => MENU_CALLBACK,
-    'file' => 'taxonomy.pages.inc',
-  );
 
   $items['admin/structure/taxonomy/manage/%taxonomy_vocabulary'] = array(
     'title callback' => 'entity_page_label',
diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc
index 4baf150..491ce50 100644
--- a/core/modules/taxonomy/taxonomy.pages.inc
+++ b/core/modules/taxonomy/taxonomy.pages.inc
@@ -6,8 +6,6 @@
  */
 
 use Drupal\taxonomy\Plugin\Core\Entity\Term;
-use Drupal\taxonomy\Plugin\Core\Entity\Vocabulary;
-use Symfony\Component\HttpFoundation\JsonResponse;
 
 /**
  * Menu callback; displays all nodes associated with a term.
@@ -78,86 +76,3 @@ function taxonomy_term_feed(Term $term) {
 
   return 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.
- *
- * @see taxonomy_menu()
- * @see taxonomy_field_widget_info()
- */
-function taxonomy_autocomplete($field_name) {
-  // A comma-separated list of term names entered in the autocomplete form
-  // element. Only the last term is used for autocompletion.
-  $tags_typed = Drupal::request()->query->get('q');
-
-  // 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();
-    foreach ($field['settings']['allowed_values'] as $tree) {
-      $vids[] = $tree['vocabulary'];
-    }
-
-    $query = db_select('taxonomy_term_data', 't');
-    $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.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml
index 09dc6d0..d89019e 100644
--- a/core/modules/taxonomy/taxonomy.routing.yml
+++ b/core/modules/taxonomy/taxonomy.routing.yml
@@ -32,3 +32,10 @@ taxonomy_vocabulary_reset:
     _entity_form: 'taxonomy_vocabulary.reset'
   requirements:
     _permission: 'administer taxonomy'
+
+taxonomy_autocomplete:
+  pattern: '/taxonomy/autocomplete/{field_name}'
+  defaults:
+    _controller: '\Drupal\taxonomy\Controller\TermAutocompleteController::autocomplete'
+  requirements:
+    _permission: 'access content'
