diff --git a/doc/api/routing.rst b/doc/api/routing.rst
index b76ba4f..c0ce2d2 100644
--- a/doc/api/routing.rst
+++ b/doc/api/routing.rst
@@ -43,4 +43,4 @@ A custom routing engine may be specified when instantiating
     api = API(router=fancy)
 
 .. automodule:: falcon.routing
-    :members: create_http_method_map, CompiledRouter
+    :members: create_http_method_map, compile_uri_template, CompiledRouter
diff --git a/falcon/request.py b/falcon/request.py
index 74915a0..b16e154 100644
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -599,7 +599,7 @@ class Request(object):
             if not required:
                 return None
 
-            raise HTTPMissingHeader(name)
+            raise HTTPMissingParam(name)
 
     def get_header_as_datetime(self, header, required=False):
         """Return an HTTP header with HTTP-Date values as a datetime.
diff --git a/falcon/routing/__init__.py b/falcon/routing/__init__.py
index 6498a47..9ced8d6 100644
--- a/falcon/routing/__init__.py
+++ b/falcon/routing/__init__.py
@@ -14,6 +14,7 @@
 
 from falcon.routing.compiled import CompiledRouter
 from falcon.routing.util import create_http_method_map  # NOQA
+from falcon.routing.util import compile_uri_template  # NOQA
 
 
 DefaultRouter = CompiledRouter
diff --git a/falcon/routing/util.py b/falcon/routing/util.py
index 40625af..e4f258e 100644
--- a/falcon/routing/util.py
+++ b/falcon/routing/util.py
@@ -12,10 +12,72 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import re
+
+import six
+
 from falcon import HTTP_METHODS, responders
 from falcon.hooks import _wrap_with_hooks
 
 
+# NOTE(kgriffs): Published method; take care to avoid breaking changes.
+def compile_uri_template(template):
+    """Compile the given URI template string into a pattern matcher.
+
+    This function can be used to construct custom routing engines that
+    iterate through a list of possible routes, attempting to match
+    an incoming request against each route's compiled regular expression.
+
+    Each field is converted to a named group, so that when a match
+    is found, the fields can be easily extracted using
+    :py:meth:`re.MatchObject.groupdict`.
+
+    This function does not support the more flexible templating
+    syntax used in the default router. Only simple paths with bracketed
+    field expressions are recognized. For example::
+
+        /
+        /books
+        /books/{isbn}
+        /books/{isbn}/characters
+        /books/{isbn}/characters/{name}
+
+    Also, note that if the template contains a trailing slash character,
+    it will be stripped in order to normalize the routing logic.
+
+    Args:
+        template(str): The template to compile. Note that field names are
+            restricted to ASCII a-z, A-Z, and the underscore character.
+
+    Returns:
+        tuple: (template_field_names, template_regex)
+    """
+
+    if not isinstance(template, six.string_types):
+        raise TypeError('uri_template is not a string')
+
+    if not template.startswith('/'):
+        raise ValueError("uri_template must start with '/'")
+
+    if '//' in template:
+        raise ValueError("uri_template may not contain '//'")
+
+    if template != '/' and template.endswith('/'):
+        template = template[:-1]
+
+    expression_pattern = r'{([a-zA-Z][a-zA-Z_]*)}'
+
+    # Get a list of field names
+    fields = set(re.findall(expression_pattern, template))
+
+    # Convert Level 1 var patterns to equivalent named regex groups
+    escaped = re.sub(r'[\.\(\)\[\]\?\*\+\^\|]', r'\\\g<0>', template)
+    pattern = re.sub(expression_pattern, r'(?P<\1>[^/]+)', escaped)
+    pattern = r'\A' + pattern + r'\Z'
+
+    return fields, re.compile(pattern, re.IGNORECASE)
+
+
 def create_http_method_map(resource, before, after):
     """Maps HTTP methods (e.g., 'GET', 'POST') to methods of a resource object.
 

Has compile_uri_template been removed?
I can't see it in the code any more.
diff --git a/tests/test_headers.py b/tests/test_headers.py
index 654eee9..f2a689a 100644
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -183,14 +183,9 @@ class TestHeaders(testing.TestBase):
     def test_required_header(self):
         self.simulate_request(self.test_route)
 
-        try:
-            self.resource.req.get_header('X-Not-Found', required=True)
-            self.fail('falcon.HTTPMissingHeader not raised')
-        except falcon.HTTPMissingHeader as ex:
-            self.assertIsInstance(ex, falcon.HTTPBadRequest)
-            self.assertEqual(ex.title, 'Missing header value')
-            expected_desc = 'The X-Not-Found header is required.'
-            self.assertEqual(ex.description, expected_desc)
+        self.assertRaises(falcon.HTTPBadRequest,
+                          self.resource.req.get_header, 'X-Not-Found',
+                          required=True)
 
     def test_no_body_on_100(self):
         self.resource = StatusTestResource(falcon.HTTP_100)
diff --git a/tests/test_query_params.py b/tests/test_query_params.py
index d6adbf8..893afda 100644
--- a/tests/test_query_params.py
+++ b/tests/test_query_params.py
@@ -94,7 +94,6 @@ class _TestQueryParams(testing.TestBase):
             getattr(req, method_name)('marker', required=True)
             self.fail('falcon.HTTPMissingParam not raised')
         except falcon.HTTPMissingParam as ex:
-            self.assertIsInstance(ex, falcon.HTTPBadRequest)
             self.assertEqual(ex.title, 'Missing query parameter')
             expected_desc = 'The "marker" query parameter is required.'
             self.assertEqual(ex.description, expected_desc)
diff --git a/tests/test_uri_templates_legacy.py b/tests/test_uri_templates_legacy.py
new file mode 100644
index 0000000..538f4be
--- /dev/null
+++ b/tests/test_uri_templates_legacy.py
@@ -0,0 +1,96 @@
+import ddt
+
+import falcon
+from falcon import routing
+import falcon.testing as testing
+
+
+@ddt.ddt
+class TestUriTemplates(testing.TestBase):
+
+    def test_string_type_required(self):
+        self.assertRaises(TypeError, routing.compile_uri_template, 42)
+        self.assertRaises(TypeError, routing.compile_uri_template, falcon.API)
+
+    def test_template_must_start_with_slash(self):
+        self.assertRaises(ValueError, routing.compile_uri_template, 'this')
+        self.assertRaises(ValueError, routing.compile_uri_template, 'this/that')
+
+    def test_template_may_not_contain_double_slash(self):
+        self.assertRaises(ValueError, routing.compile_uri_template, '//')
+        self.assertRaises(ValueError, routing.compile_uri_template, 'a//')
+        self.assertRaises(ValueError, routing.compile_uri_template, '//b')
+        self.assertRaises(ValueError, routing.compile_uri_template, 'a//b')
+        self.assertRaises(ValueError, routing.compile_uri_template, 'a/b//')
+        self.assertRaises(ValueError, routing.compile_uri_template, 'a/b//c')
+
+    def test_root(self):
+        fields, pattern = routing.compile_uri_template('/')
+        self.assertFalse(fields)
+        self.assertFalse(pattern.match('/x'))
+
+        result = pattern.match('/')
+        self.assertTrue(result)
+        self.assertFalse(result.groupdict())
+
+    @ddt.data('/hello', '/hello/world', '/hi/there/how/are/you')
+    def test_no_fields(self, path):
+        fields, pattern = routing.compile_uri_template(path)
+        self.assertFalse(fields)
+        self.assertFalse(pattern.match(path[:-1]))
+
+        result = pattern.match(path)
+        self.assertTrue(result)
+        self.assertFalse(result.groupdict())
+
+    def test_one_field(self):
+        fields, pattern = routing.compile_uri_template('/{name}')
+        self.assertEqual(fields, set(['name']))
+
+        result = pattern.match('/Kelsier')
+        self.assertTrue(result)
+        self.assertEqual(result.groupdict(), {'name': 'Kelsier'})
+
+        fields, pattern = routing.compile_uri_template('/character/{name}')
+        self.assertEqual(fields, set(['name']))
+
+        result = pattern.match('/character/Kelsier')
+        self.assertTrue(result)
+        self.assertEqual(result.groupdict(), {'name': 'Kelsier'})
+
+        fields, pattern = routing.compile_uri_template('/character/{name}/profile')
+        self.assertEqual(fields, set(['name']))
+
+        self.assertFalse(pattern.match('/character'))
+        self.assertFalse(pattern.match('/character/Kelsier'))
+        self.assertFalse(pattern.match('/character/Kelsier/'))
+
+        result = pattern.match('/character/Kelsier/profile')
+        self.assertTrue(result)
+        self.assertEqual(result.groupdict(), {'name': 'Kelsier'})
+
+    @ddt.data('', '/')
+    def test_two_fields(self, postfix):
+        path = '/book/{id}/characters/{name}' + postfix
+        fields, pattern = routing.compile_uri_template(path)
+        self.assertEqual(fields, set(['name', 'id']))
+
+        result = pattern.match('/book/0765350386/characters/Vin')
+        self.assertTrue(result)
+        self.assertEqual(result.groupdict(), {'name': 'Vin', 'id': '0765350386'})
+
+    def test_three_fields(self):
+        fields, pattern = routing.compile_uri_template('/{a}/{b}/x/{c}')
+        self.assertEqual(fields, set('abc'))
+
+        result = pattern.match('/one/2/x/3')
+        self.assertTrue(result)
+        self.assertEqual(result.groupdict(), {'a': 'one', 'b': '2', 'c': '3'})
+
+    def test_malformed_field(self):
+        fields, pattern = routing.compile_uri_template('/{a}/{1b}/x/{c}')
+        self.assertEqual(fields, set('ac'))
+
+        result = pattern.match('/one/{1b}/x/3')
+        self.assertTrue(result)
+        self.assertEqual(result.groupdict(), {'a': 'one', 'c': '3'})

pip install -e .
parse_log_pytest
pytest
nose coverage ddt pyyaml requests testtools pytest
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
coverage==7.2.7
ddt==1.7.2
-e git+https://github.com/falconry/falcon.git@6912cdc1232754f8e8be4bd25ce150f9485012c2#egg=falcon
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
nose==1.3.7
packaging @ file:///croot/packaging_1671697413597/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pytest==7.1.2
python-mimeparse==1.6.0
PyYAML==6.0.1
requests==2.31.0
six==1.17.0
testtools==2.7.1
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
urllib3==2.0.7
zipp @ file:///croot/zipp_1672387121353/work

name: falcon
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - coverage==7.2.7
      - ddt==1.7.2
      - idna==3.10
      - nose==1.3.7
      - python-mimeparse==1.6.0
      - pyyaml==6.0.1
      - requests==2.31.0
      - six==1.17.0
      - testtools==2.7.1
      - urllib3==2.0.7
prefix: /opt/conda/envs/falcon

tests/test_uri_templates_legacy.py::TestUriTemplates::test_malformed_field tests/test_uri_templates_legacy.py::TestUriTemplates::test_no_fields_1__hello tests/test_uri_templates_legacy.py::TestUriTemplates::test_no_fields_2__hello_world tests/test_uri_templates_legacy.py::TestUriTemplates::test_no_fields_3__hi_there_how_are_you tests/test_uri_templates_legacy.py::TestUriTemplates::test_one_field tests/test_uri_templates_legacy.py::TestUriTemplates::test_root tests/test_uri_templates_legacy.py::TestUriTemplates::test_string_type_required tests/test_uri_templates_legacy.py::TestUriTemplates::test_template_may_not_contain_double_slash tests/test_uri_templates_legacy.py::TestUriTemplates::test_template_must_start_with_slash tests/test_uri_templates_legacy.py::TestUriTemplates::test_three_fields tests/test_uri_templates_legacy.py::TestUriTemplates::test_two_fields_1_ tests/test_uri_templates_legacy.py::TestUriTemplates::test_two_fields_2__
tests/test_headers.py::TestHeaders::test_add_link_complex tests/test_headers.py::TestHeaders::test_add_link_multiple tests/test_headers.py::TestHeaders::test_add_link_single tests/test_headers.py::TestHeaders::test_add_link_with_anchor tests/test_headers.py::TestHeaders::test_add_link_with_hreflang tests/test_headers.py::TestHeaders::test_add_link_with_hreflang_multi tests/test_headers.py::TestHeaders::test_add_link_with_title tests/test_headers.py::TestHeaders::test_add_link_with_title_star tests/test_headers.py::TestHeaders::test_add_link_with_type_hint tests/test_headers.py::TestHeaders::test_content_header_missing tests/test_headers.py::TestHeaders::test_content_length tests/test_headers.py::TestHeaders::test_custom_content_type tests/test_headers.py::TestHeaders::test_custom_media_type tests/test_headers.py::TestHeaders::test_default_media_type tests/test_headers.py::TestHeaders::test_default_value tests/test_headers.py::TestHeaders::test_get_raw_headers tests/test_headers.py::TestHeaders::test_no_body_on_100 tests/test_headers.py::TestHeaders::test_no_body_on_101 tests/test_headers.py::TestHeaders::test_no_body_on_204 tests/test_headers.py::TestHeaders::test_no_body_on_304 tests/test_headers.py::TestHeaders::test_no_content_type tests/test_headers.py::TestHeaders::test_passthrough_req_headers tests/test_headers.py::TestHeaders::test_passthrough_resp_headers tests/test_headers.py::TestHeaders::test_required_header tests/test_headers.py::TestHeaders::test_response_append_header tests/test_headers.py::TestHeaders::test_response_header_helpers_on_get tests/test_headers.py::TestHeaders::test_response_set_header tests/test_headers.py::TestHeaders::test_unicode_location_headers tests/test_headers.py::TestHeaders::test_vary_header tests/test_headers.py::TestHeaders::test_vary_headers tests/test_headers.py::TestHeaders::test_vary_headers_tuple tests/test_headers.py::TestHeaders::test_vary_star tests/test_query_params.py::_TestQueryParams::test_allowed_names tests/test_query_params.py::_TestQueryParams::test_blank tests/test_query_params.py::_TestQueryParams::test_boolean tests/test_query_params.py::_TestQueryParams::test_boolean_blank tests/test_query_params.py::_TestQueryParams::test_get_date_invalid tests/test_query_params.py::_TestQueryParams::test_get_date_missing_param tests/test_query_params.py::_TestQueryParams::test_get_date_store tests/test_query_params.py::_TestQueryParams::test_get_date_valid tests/test_query_params.py::_TestQueryParams::test_get_date_valid_with_format tests/test_query_params.py::_TestQueryParams::test_int tests/test_query_params.py::_TestQueryParams::test_int_neg tests/test_query_params.py::_TestQueryParams::test_list_transformer tests/test_query_params.py::_TestQueryParams::test_list_type tests/test_query_params.py::_TestQueryParams::test_list_type_blank tests/test_query_params.py::_TestQueryParams::test_multiple_form_keys tests/test_query_params.py::_TestQueryParams::test_multiple_form_keys_as_list tests/test_query_params.py::_TestQueryParams::test_multiple_keys_as_bool tests/test_query_params.py::_TestQueryParams::test_multiple_keys_as_int tests/test_query_params.py::_TestQueryParams::test_none tests/test_query_params.py::_TestQueryParams::test_param_property tests/test_query_params.py::_TestQueryParams::test_percent_encoded tests/test_query_params.py::_TestQueryParams::test_required_1_get_param tests/test_query_params.py::_TestQueryParams::test_required_2_get_param_as_int tests/test_query_params.py::_TestQueryParams::test_required_3_get_param_as_bool tests/test_query_params.py::_TestQueryParams::test_required_4_get_param_as_list tests/test_query_params.py::_TestQueryParams::test_simple tests/test_query_params.py::PostQueryParams::test_allowed_names tests/test_query_params.py::PostQueryParams::test_blank tests/test_query_params.py::PostQueryParams::test_boolean tests/test_query_params.py::PostQueryParams::test_boolean_blank tests/test_query_params.py::PostQueryParams::test_get_date_invalid tests/test_query_params.py::PostQueryParams::test_get_date_missing_param tests/test_query_params.py::PostQueryParams::test_get_date_store tests/test_query_params.py::PostQueryParams::test_get_date_valid tests/test_query_params.py::PostQueryParams::test_get_date_valid_with_format tests/test_query_params.py::PostQueryParams::test_int tests/test_query_params.py::PostQueryParams::test_int_neg tests/test_query_params.py::PostQueryParams::test_list_transformer tests/test_query_params.py::PostQueryParams::test_list_type tests/test_query_params.py::PostQueryParams::test_list_type_blank tests/test_query_params.py::PostQueryParams::test_multiple_form_keys tests/test_query_params.py::PostQueryParams::test_multiple_form_keys_as_list tests/test_query_params.py::PostQueryParams::test_multiple_keys_as_bool tests/test_query_params.py::PostQueryParams::test_multiple_keys_as_int tests/test_query_params.py::PostQueryParams::test_non_ascii tests/test_query_params.py::PostQueryParams::test_none tests/test_query_params.py::PostQueryParams::test_param_property tests/test_query_params.py::PostQueryParams::test_percent_encoded tests/test_query_params.py::PostQueryParams::test_required_1_get_param tests/test_query_params.py::PostQueryParams::test_required_2_get_param_as_int tests/test_query_params.py::PostQueryParams::test_required_3_get_param_as_bool tests/test_query_params.py::PostQueryParams::test_required_4_get_param_as_list tests/test_query_params.py::PostQueryParams::test_simple tests/test_query_params.py::GetQueryParams::test_allowed_names tests/test_query_params.py::GetQueryParams::test_blank tests/test_query_params.py::GetQueryParams::test_boolean tests/test_query_params.py::GetQueryParams::test_boolean_blank tests/test_query_params.py::GetQueryParams::test_get_date_invalid tests/test_query_params.py::GetQueryParams::test_get_date_missing_param tests/test_query_params.py::GetQueryParams::test_get_date_store tests/test_query_params.py::GetQueryParams::test_get_date_valid tests/test_query_params.py::GetQueryParams::test_get_date_valid_with_format tests/test_query_params.py::GetQueryParams::test_int tests/test_query_params.py::GetQueryParams::test_int_neg tests/test_query_params.py::GetQueryParams::test_list_transformer tests/test_query_params.py::GetQueryParams::test_list_type tests/test_query_params.py::GetQueryParams::test_list_type_blank tests/test_query_params.py::GetQueryParams::test_multiple_form_keys tests/test_query_params.py::GetQueryParams::test_multiple_form_keys_as_list tests/test_query_params.py::GetQueryParams::test_multiple_keys_as_bool tests/test_query_params.py::GetQueryParams::test_multiple_keys_as_int tests/test_query_params.py::GetQueryParams::test_none tests/test_query_params.py::GetQueryParams::test_param_property tests/test_query_params.py::GetQueryParams::test_percent_encoded tests/test_query_params.py::GetQueryParams::test_required_1_get_param tests/test_query_params.py::GetQueryParams::test_required_2_get_param_as_int tests/test_query_params.py::GetQueryParams::test_required_3_get_param_as_bool tests/test_query_params.py::GetQueryParams::test_required_4_get_param_as_list tests/test_query_params.py::GetQueryParams::test_simple
falcon/request.py falcon/routing/util.py falcon/routing/__init__.py doc/api/routing.rst
falcon/request.py falcon/routing/util.py falcon/routing/__init__.py doc/api/routing.rst