diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyAutocomplete.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyAutocomplete.php
new file mode 100644
index 0000000..76f7575
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyAutocomplete.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\TaxonomyAutocomplete.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Database\Connection;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines a helper class to get taxonomy autocompletion results.
+ */
+class TaxonomyAutocomplete {
+
+  /**
+   * The request object to get the autocomplete query from.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The database connection to query for terms.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs an TaxonomyAutocomplete object.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object to get the autocomplete query from.
+   * @param\Drupal\Core\Database\Connection $connection
+   *   The database connection to query for terms.
+   */
+  public function __construct(Request $request, Connection $connection) {
+    $this->request = $request;
+    $this->connection = $connection;
+  }
+
+  /**
+   * Router callback to output JSON for taxonomy autcomplete 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.
+   *
+   * @throws \Exception
+   *
+   * @return array
+   *   The matches as array.
+   *
+   * @see taxonomy_field_widget_info()
+   */
+  public function getMatches($field_name) {
+
+    $tags_typed = $this->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.
+      throw new \Exception(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));
+
+    $term_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 = $this->connection->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) . ', ' : '';
+
+      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 $term_matches;
+  }
+
+}
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..4b7fac0
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TaxonomyRouteController.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\taxonomy\TaxonomyRouteController.
+ */
+
+namespace Drupal\taxonomy;
+
+use Symfony\Component\DependencyInjection\ContainerAware;
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+/**
+ * Controller routines for taxonomy routes.
+ */
+class TaxonomyRouteController extends ContainerAware {
+
+  /**
+   * Autocompletes a taxonomy term.
+   *
+   * @param string $field_name
+   *   The name of the term reference field.
+   * @param string $tags_typed
+   *   String with tags to search for.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The actual response for the javascript.
+   */
+  public function autocomplete($field_name, $tags_typed = '') {
+    $taxonomy = new TaxonomyAutocomplete($this->container->get('request'), $this->container->get('database'));
+    try {
+      $matches = $taxonomy->getMatches($field_name, $tags_typed);
+      return new JsonResponse($matches);
+    }
+    catch (\Exception $e) {
+      return new Response($e->getMessage());
+    }
+  }
+
+}
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 16d1019..fb75e67 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -10,6 +10,10 @@
 use Drupal\taxonomy\Plugin\Core\Entity\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.
  */
@@ -321,13 +325,16 @@ function taxonomy_menu() {
     'type' => MENU_CALLBACK,
     'file' => 'taxonomy.pages.inc',
   );
+
+  // @todo Remove once drupal_valid_path() is fixed to find and access check
+  //   paths managed by the new routing system: http://drupal.org/node/1793520.
   $items['taxonomy/autocomplete/%'] = array(
     'title' => 'Autocomplete taxonomy',
-    'page callback' => 'taxonomy_autocomplete',
+    // _menu_router_build() denies access to paths without a page callback.
+    'page callback' => 'NOT_USED',
     'page arguments' => array(2),
     'access arguments' => array('access content'),
     'type' => MENU_CALLBACK,
-    'file' => 'taxonomy.pages.inc',
   );
 
   $items['admin/structure/taxonomy/%taxonomy_vocabulary'] = array(
diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc
index fbd29f1..75558af 100644
--- a/core/modules/taxonomy/taxonomy.pages.inc
+++ b/core/modules/taxonomy/taxonomy.pages.inc
@@ -77,87 +77,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_container()->get('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('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.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml
new file mode 100644
index 0000000..5cca0f5
--- /dev/null
+++ b/core/modules/taxonomy/taxonomy.routing.yml
@@ -0,0 +1,6 @@
+taxonomy_autocomplete:
+  pattern: '/taxonomy/autocomplete/{field_name}'
+  defaults:
+    _controller: '\Drupal\taxonomy\TaxonomyRouteController::autocomplete'
+  requirements:
+    _permission: 'access content'
