URL Pattern API: Руководство по нативной маршрутизации в JavaScript

URL Pattern API — это как регулярные выражения для адресной строки, только проще. Разбираем синтаксис, три главных метода и сценарии использования: от умного кэширования в Service Worker до роутинга на сервере.

Введение

Разработчики постоянно сталкиваются с задачей анализа URL. Типичный сценарий — определение типа страницы, на которой находится пользователь, и извлечение параметров из адресной строки. Чаще всего это решается цепочкой манипуляций с window.location.pathname: ручной разбор через split('/'), поиск подстрок и хрупкие регулярные выражения.

Классический подход выглядит так:

// Хрупкий и сложно поддерживаемый код
const path = window.location.pathname; // "/products/123/edit"
const parts = path.split('/'); // ['', 'products', '123', 'edit']
if (parts[1] === 'products' && parts[3] === 'edit') {
const productId = parts[2];
console.log('Редактируем товар:', productId);
}

Этот код страдает рядом недостатков: он чувствителен к структуре URL, плохо читается и требует существенной доработки при любом изменении маршрутов. Поддержка опциональных параметров или более сложных паттернов делает такой подход практически непригодным для production-решений.

URL Pattern API: стандартизированное решение

С сентября 2025 года у JavaScript-разработчиков появился инструмент, который элегантно решает эту проблему — URL Pattern API. Это стандартизированный интерфейс для описания шаблонов URL, встроенный непосредственно в браузеры и серверные JavaScript-окружения (например, Deno). Его синтаксис основан на широко известной библиотеке path-to-regexp.

API предоставляет две ключевые возможности:

  • Валидация URL на соответствие заданному шаблону.
  • Извлечение именованных параметров из URL.

Предыдущий пример, реализованный с использованием URL Pattern API:

const pattern = new URLPattern('/products/:id/edit');
const match = pattern.exec(window.location.href);

if (match) {
const productId = match.pathname.groups.id; // "123"
console.log('Редактируем товар:', productId);
}

Такой подход обеспечивает читаемость, надёжность и простоту сопровождения кода.

Важно отметить, что API достиг статуса Baseline Newly Available. Это означает, что технология поддерживается во всех современных браузерах и может использоваться в production-окружениях.

В статье мы рассмотрим практическое применение URL Pattern API: от базового синтаксиса до реальных сценариев использования в клиентских и серверных приложениях.

Синтаксис: Как читать и писать

Освоившись с базовой идеей, давайте детально разберём синтаксис URL Pattern API. Понимание того, как строятся паттерны, позволит вам эффективно использовать этот инструмент в реальных задачах — от простой валидации до сложной маршрутизации.

Базовые конструкции

В основе любого паттерна лежат два типа элементов: фиксированный текст (который должен совпадать буквально) и группы захвата (которые соответствуют переменной части URL).

Точное совпадение

Простейший случай — паттерн, состоящий только из фиксированного текста:

// Совпадает только с точным путём /products/featured
const pattern = new URLPattern({ pathname: '/products/featured' });

console.log(pattern.test('https://site.com/products/featured')); // true
console.log(pattern.test('https://site.com/products/new')); // false

Подстановочный знак *

Символ * соответствует любой последовательности символов любой длины (включая пустую). Это жадное соответствие — паттерн захватит максимально возможную строку.

// Любой путь, начинающийся с /posts/
const pattern = new URLPattern({ pathname: '/posts/*' });

console.log(pattern.test('https://site.com/posts/')); // true
console.log(pattern.test('https://site.com/posts/123')); // true
console.log(pattern.test('https://site.com/posts/2025/03/28')); // true

Именованные группы (:id)

Часто используемая конструкция. Именованные группы позволяют не только сопоставить URL с шаблоном, но и извлечь конкретные значения:

const pattern = new URLPattern('/users/:userId/profile');

const match = pattern.exec('https://site.com/users/42/profile');
if (match) {
console.log(match.pathname.groups.userId); // "42"
}

Модификаторы и опциональность

Группы можно делать опциональными или повторяющимися с помощью модификаторов, знакомых по регулярным выражениям.

  • ? - Ноль или одно вхождение (опционально)
  • + - Одно или более вхождений
  • * - Ноль или более вхождений

Опциональные группы (?)

// Страницы товара могут быть как с ID, так и без (например, /books/)
const pattern = new URLPattern('/books/:id?');

console.log(pattern.test('https://site.com/books/')); // true
console.log(pattern.test('https://site.com/books/123')); // true

Повторяющиеся сегменты (+ и *)

Модификаторы удобны для работы с вложенными путями:

// Один или более сегментов пути
const pattern = new URLPattern('/files/:segment+');

console.log(pattern.exec('https://site.com/files/docs/2025/report.pdf'));
// match.pathname.groups.segment = "docs/2025/report.pdf"

// Ноль или более сегментов
const flexiblePattern = new URLPattern('/files/:segment*');
console.log(flexiblePattern.test('https://site.com/files/')); // true

Группы-разделители ({})

Фигурные скобки позволяют группировать части паттерна без создания захватывающей группы. Это удобно для создания опциональных блоков, которые не должны попадать в результат.

// Паттерн соответствует /blog и /blog/page/2
// при этом "page/2" не создаёт отдельной группы в результате
const pattern = new URLPattern('/blog{/page/:p}?');

const match = pattern.exec('https://site.com/blog/page/3');
console.log(match.pathname.groups.p); // "3" - группа существует
// Но сам блок {/page/:p} не захватывается как единое целое

Регулярные выражения внутри паттернов

Для более точного контроля над форматом значений можно использовать регулярные выражения. Они указываются в круглых скобках после имени группы или вместо него.

// Только числовые ID товаров
const pattern = new URLPattern('/products/(\\d+)');

console.log(pattern.test('/products/123')); // true
console.log(pattern.test('/products/abc')); // false

// Именованная группа с regex-ограничением
const namedPattern = new URLPattern('/users/:id(\\d+)');
const match = namedPattern.exec('/users/456');
console.log(match.pathname.groups.id); // "456"

Особенности работы с регулярными выражениями:

  1. Якоря ^ и $ не требуются. URL Pattern API автоматически подразумевает, что паттерн должен соответствовать всей строке от начала до конца. Следующие паттерны эквивалентны:

    new URLPattern('/books/\\d+')
    new URLPattern('^/books/\\d+$')
  2. Экранирование. Внутри паттерна действуют свои правила экранирования, отличные от стандартных регулярных выражений. Классический пример — диапазоны символов. То, что работает в обычном RegExp, может потребовать дополнительного экранирования в URL Pattern:

    // Стандартный RegExp: квадратные скобки работают как обычно
    const regex = /[()]/;
    console.log(regex.test('(')); // true
    console.log(regex.test(')')); // true

    // URL Pattern: так НЕ РАБОТАЕТ (будет синтаксическая ошибка)
    // new URLPattern({ pathname: "([()])" }); // throws

    // Правильное экранирование в URL Pattern
    const pattern = new URLPattern({ pathname: "([\\(\\)])" });
    console.log(pattern.test('/(')); // true
    console.log(pattern.test('/)')); // true
    console.log(pattern.test('/a')); // false
  3. Lookahead и lookbehind работают, но требуют осторожности. Паттерн должен потребить все символы проверяемой строки:

    // НЕ РАБОТАЕТ КАК ОЖИДАЕТСЯ
    const badPattern = new URLPattern({ pathname: '/a(?=b)' });
    // /ab - не совпадёт!

    // РАБОЧИЙ ВАРИАНТ - нужно потребить 'b'
    const goodPattern = new URLPattern({ pathname: '/a(?=b)b' });
    console.log(goodPattern.test('/ab')); // true

Мы рассмотрели основные строительные блоки URL Pattern API:

  • Фиксированный текст для точного совпадения
  • Именованные группы для извлечения данных
  • Wildcard для гибкого сопоставления
  • Модификаторы для создания опциональных и повторяющихся частей
  • Регулярные выражения для точной валидации формата

Этих конструкций достаточно для подавляющего большинства практических задач. В следующем разделе мы перейдём от теории к практике и подробно разберём методы API для создания паттернов и работы с ними.

Работа с API: Три главных метода

Теперь, когда мы разобрались с синтаксисом, посмотрим на практическое использование API. Взаимодействие строится вокруг одного интерфейса — URLPattern — и трёх его основных методов.

Создание паттерна (URLPattern)

Конструктор URLPattern можно вызывать двумя способами: со строкой или с объектом конфигурации.

Строковый конструктор

Наиболее лаконичный способ, когда паттерн описывает полный URL или его относительную часть:

// Полный URL с паттернами
const pattern1 = new URLPattern('https://:site.com/:page');

// Относительный путь (будет применён к текущему базовому URL)
const pattern2 = new URLPattern('/products/:id');

// С явным указанием базового URL
const pattern3 = new URLPattern('/:page', 'https://example.com');

Обратите внимание на потенциальную неоднозначность: символ : может быть как частью протокола (https:), так и началом именованной группы (:site). Конструктор разрешает эту неоднозначность в пользу синтаксиса паттерна. Если вам нужно экранировать : как часть протокола, используйте обратный слэш:

// Корректное экранирование протокола
const pattern = new URLPattern('https\\://:site.com/:page');

Объектный конструктор

Более гибкий подход, когда вы точно указываете, к каким компонентам URL применяются паттерны:

const pattern = new URLPattern({
protocol: 'https',
hostname: ':subdomain.:domain.:tld',
pathname: '/:entity/:id?',
search: '?:query?',
hash: ':*'
});

const match = pattern.exec('https://cdn.example.com/products/123?search=book#section');
console.log(match.protocol.groups); // {} (протокол совпал точно)
console.log(match.hostname.groups); // { subdomain: 'cdn', domain: 'example', tld: 'com' }
console.log(match.pathname.groups); // { entity: 'products', id: '123' }
console.log(match.search.groups); // { query: 'search=book' }
console.log(match.hash.groups); // { '0': 'section' } (безымянная группа)

Любые пропущенные компоненты автоматически получают значение * (wildcard), что делает их опциональными.

Проверка соответствия (test())

Метод test() возвращает логическое значение — подходит ли URL под заданный паттерн. Это самый быстрый способ проверки, когда вам не нужно извлекать данные.

const pattern = new URLPattern('/posts/:year/:month/:day?');

// Проверка по строке
if (pattern.test('https://blog.example/posts/2025/03')) {
console.log('Это пост за март 2025');
}

// Проверка с базовым URL
if (pattern.test('/posts/2025', 'https://blog.example')) {
console.log('Валидный путь к постам');
}

// Проверка по объекту URL
const url = new URL('https://blog.example/posts/2025/03/28');
if (pattern.test(url)) {
console.log('Полная дата указана');
}

Метод принимает те же типы аргументов, что и конструктор: строку (возможно с базовым URL) или объект с компонентами.

Извлечение данных (exec())

Метод exec() — рабочая лошадка API. Он возвращает либо null (если URL не соответствует паттерну), либо объект с подробной информацией о совпадении.

const pattern = new URLPattern('/api/:version/:resource/:id?');

const result = pattern.exec('https://api.service.com/api/v2/users/42');

if (result) {
console.log(result.input); // 'https://api.service.com/api/v2/users/42'

// Группы по компонентам URL
console.log(result.pathname.groups); // { version: 'v2', resource: 'users', id: '42' }

// Доступ к конкретному компоненту
console.log(result.hostname.groups); // {} (хост совпал точно, групп нет)

// Для безымянных групп используются числовые индексы
// (в этом паттерне все группы именованные, поэтому groups[0] отсутствует)
}

Структура результата включает:

  • input — исходная строка, которая проверялась
  • Компоненты URL (protocol, hostname, port, pathname, search, hash, username, password) — каждый содержит:
    • input: часть URL, соответствующая данному компоненту
    • groups: объект с захваченными значениями (по именам и числовым индексам)

Именованные vs безымянные группы

// Паттерн со смешанными группами
const pattern = new URLPattern('/:category/(.*)/:id(\\d+)');
const result = pattern.exec('/books/fiction/42');

if (result) {
console.log(result.pathname.groups.category); // 'books'
console.log(result.pathname.groups[0]); // 'fiction' (первая безымянная группа)
console.log(result.pathname.groups.id); // '42'
}

Именованные группы всегда доступны по своему имени. Безымянные группы (wildcard, regex-группы без имени) доступны по числовым индексам в порядке их появления в паттерне.

Работа с базовым URL

Важная особенность API — возможность наследовать компоненты URL от базового адреса. Это работает как при создании паттерна, так и при проверке URL.

// Базовый URL задаётся вторым аргументом конструктора
const pattern = new URLPattern('/:page', 'https://example.com/blog');

console.log(pattern.protocol); // '*' - протокол не указан в паттерне, наследуется при выполнении
// На самом деле pattern.protocol вернёт '*' потому что в объектном представлении
// несвязанные компоненты - это wildcard. Наследование работает в момент test/exec.

// При проверке базовый URL дополняет недостающие части
const result = pattern.exec('/about', 'https://example.com/blog');
// Результат будет содержать данные так, как будто проверялся
// полный URL 'https://example.com/blog/about'

Правила наследования:

  • Наследуются только те компоненты, которые менее специфичны, чем указанные в проверяемом URL
  • Порядок специфичности (от наиболее к наименее): protocol, hostname, port, pathname, search, hash (и отдельно для авторизации: username, password)
  • username и password никогда не наследуются

Пример для понимания специфичности:

const base = 'https://example.com:8080/blog?query=1#hash';
const pattern = new URLPattern('/:page', base);

// Проверяем только pathname - наследуются protocol, hostname, port
const result1 = pattern.exec('/about', base);
// Фактически проверяется: 'https://example.com:8080/about'

// Проверяем pathname и search - наследуются protocol, hostname, port
// но search НЕ наследуется (он указан явно)
const result2 = pattern.exec('/about?debug=true', base);
// Фактически: 'https://example.com:8080/about?debug=true'

Три метода API покрывают все сценарии использования:

  • Конструктор — определяет шаблон одним из двух способов
  • test() — быстро проверяет соответствие
  • exec() — извлекает структурированные данные из URL

В следующем разделе мы применим эти методы для решения реальных задач, с которыми сталкивается каждый разработчик.

Сценарии использования

Теория — это хорошо, но давайте посмотрим, как URL Pattern API работает в реальных проектах. Рассмотрим три типичных сценария: клиентская маршрутизация в Service Worker, vanilla JS-приложение без фреймворков и сервер на Deno.

Сценарий: Умный Service Worker

Service Worker — идеальное место для применения URL Pattern API. Представьте, что мы строим PWA для интернет-магазина и хотим кэшировать разные типы страниц по-разному:

// sw.js
self.addEventListener('install', (event) => {
// Предварительное кэширование статики
event.waitUntil(
caches.open('static-v1').then((cache) =>
cache.addAll(['/offline.html', '/styles.css', '/app.js'])
)
);
});

self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);

// Определяем паттерны для разных разделов
const patterns = {
product: new URLPattern({ pathname: '/products/:id' }),
category: new URLPattern({ pathname: '/categories/:name' }),
search: new URLPattern({ pathname: '/search', search: '*q=*' }),
static: new URLPattern({ pathname: '/*.(css|js|png|jpg)' })
};

// Стратегия кэширования зависит от типа контента
if (patterns.product.test(url)) {
// Страницы товаров: сначала кэш, потом сеть
event.respondWith(handleProductPage(event.request));
} else if (patterns.category.test(url)) {
// Категории: сначала сеть, обновляем кэш
event.respondWith(handleCategoryPage(event.request));
} else if (patterns.search.test(url)) {
// Поиск: только сеть (данные всегда свежие)
event.respondWith(fetch(event.request));
} else if (patterns.static.test(url)) {
// Статика: только кэш с fallback на сеть
event.respondWith(handleStaticAsset(event.request));
} else {
// Всё остальное: fallback
event.respondWith(handleOtherRequests(event.request));
}
});

async function handleProductPage(request) {
const cache = await caches.open('products-v1');
const cached = await cache.match(request);

if (cached) return cached;

try {
const response = await fetch(request);
// Кэшируем страницы товаров на 1 час
cache.put(request, response.clone());
return response;
} catch {
return caches.match('/offline.html');
}
}

async function handleCategoryPage(request) {
const cache = await caches.open('categories-v1');
try {
const response = await fetch(request);
// Обновляем кэш при каждом запросе
cache.put(request, response.clone());
return response;
} catch {
const cached = await cache.match(request);
return cached || caches.match('/offline.html');
}
}

async function handleStaticAsset(request) {
const cache = await caches.open('static-v1');
const cached = await cache.match(request);
if (cached) return cached;

try {
const response = await fetch(request);
cache.put(request, response.clone());
return response;
} catch {
return new Response('Asset not found', { status: 404 });
}
}

Основные моменты:

  • Паттерны определяются один раз при инициализации и переиспользуются
  • Логика маршрутизации отделена от логики кэширования
  • Код легко расширять новыми типами страниц

Сценарий: Клиентская маршрутизация в vanilla JS-приложении

Представим, что мы пишем простое SPA без фреймворков, но с нормальной клиентской маршрутизацией:

// router.js
class Router {
constructor() {
this.routes = [];
this.currentPath = window.location.pathname;

// Перехватываем клики по ссылкам
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (link && link.href && link.origin === window.location.origin) {
e.preventDefault();
this.navigate(link.pathname + link.search + link.hash);
}
});

// Обрабатываем навигацию браузера
window.addEventListener('popstate', () => {
this.handleRoute(window.location.pathname);
});

// Обрабатываем текущий URL
this.handleRoute(this.currentPath);
}

// Добавление маршрута
addRoute(pattern, handler, options = {}) {
this.routes.push({
pattern: new URLPattern(pattern, window.location.origin),
handler,
options
});
}

// Навигация программно
navigate(path) {
window.history.pushState(null, '', path);
this.handleRoute(path);
}

// Обработка маршрута
handleRoute(path) {
const url = new URL(path, window.location.origin);

for (const route of this.routes) {
const match = route.pattern.exec(url);

if (match) {
// Извлекаем параметры из всех компонентов URL
const params = {
...match.pathname.groups,
...match.search.groups,
...match.hash.groups
};

// Вызываем обработчик с параметрами
route.handler(params, match);
return;
}
}

// 404 - ни один маршрут не подошёл
this.renderNotFound();
}

// Простой рендеринг (в реальном приложении здесь будет что-то посложнее)
render(template) {
document.getElementById('app').innerHTML = template;
}

renderNotFound() {
this.render('<h1>404 — Страница не найдена</h1>');
}
}

// Использование
const router = new Router();

// Определяем маршруты
router.addRoute('/products/:id', (params) => {
fetch(`/api/products/${params.id}`)
.then(res => res.json())
.then(product => {
router.render(`
<h1>
${product.name}</h1>
<p>
${product.description}</p>
<price>
${product.price} ₽</price>
`
);
});
});

router.addRoute('/categories/:name/page/:page?', (params) => {
const page = params.page || 1;
router.render(`<h1>Категория: ${params.name}</h1><p>Страница ${page}</p>`);
});

router.addRoute('/search?:query', (params) => {
router.render(`<h1>Поиск: ${params.query || 'все товары'}</h1>`);
});

// Маршрут с regex для числового ID
router.addRoute('/users/(\\d+)', (params) => {
// params[0] содержит значение числовой группы
router.render(`<h1>Пользователь ID: ${params[0]}</h1>`);
});

Основные моменты:

  • Паттерны создаются относительно текущего origin
  • Параметры собираются из всех частей URL (path, query, hash)
  • Безымянные группы доступны через числовые индексы

Сценарий: HTTP-сервер на Deno

URL Pattern API доступен не только в браузерах. Современные JavaScript-рантаймы, включая Deno и Bun, поддерживают этот стандарт. Вот пример простого REST API сервера:

// server.ts
import { serve } from "https://deno.land/std/http/server.ts";

// База данных в памяти
const products = new Map([
['1', { id: '1', name: 'Ноутбук', price: 120000 }],
['2', { id: '2', name: 'Мышь', price: 3500 }],
]);

// Определяем маршруты
const routes = [
{
pattern: new URLPattern('/api/products'),
method: 'GET',
handler: () => new Response(JSON.stringify([...products.values()]), {
headers: { 'content-type': 'application/json' }
})
},
{
pattern: new URLPattern('/api/products/:id'),
method: 'GET',
handler: (match: URLPatternResult) => {
const id = match.pathname.groups.id;
const product = products.get(id);

if (product) {
return new Response(JSON.stringify(product), {
headers: { 'content-type': 'application/json' }
});
}

return new Response('Product not found', { status: 404 });
}
},
{
pattern: new URLPattern('/api/products/:id'),
method: 'POST',
handler: async (match: URLPatternResult, req: Request) => {
const id = match.pathname.groups.id;
const body = await req.json();

products.set(id, { id, ...body });

return new Response(JSON.stringify({ success: true }), {
headers: { 'content-type': 'application/json' }
});
}
},
{
pattern: new URLPattern('/api/search?:query(.*)'),
method: 'GET',
handler: (match: URLPatternResult) => {
const query = match.search.groups.query || '';
const results = [...products.values()]
.filter(p => p.name.toLowerCase().includes(query.toLowerCase()));

return new Response(JSON.stringify(results), {
headers: { 'content-type': 'application/json' }
});
}
}
];

async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);

// Ищем подходящий маршрут
for (const route of routes) {
if (req.method !== route.method) continue;

const match = route.pattern.exec(url);
if (match) {
try {
return await route.handler(match, req);
} catch (err) {
return new Response(`Server error: ${err.message}`, { status: 500 });
}
}
}

return new Response('Not found', { status: 404 });
}

console.log('Server running on http://localhost:8000');
await serve(handler, { port: 8000 });

Основные моменты:

  • Один и тот же паттерн используется для разных HTTP-методов
  • Regex-группы позволяют валидировать параметры запроса
  • Код остаётся читаемым даже при росте количества маршрутов

Как видно из примеров, URL Pattern API органично вписывается в различные архитектуры:

  • Service Worker — для умного кэширования и офлайн-режимов
  • Клиентские приложения — как легковесная альтернатива тяжёлым роутерам
  • Серверный код — для чистой и декларативной маршрутизации

Общий паттерн использования везде одинаков: определяем паттерны, применяем test() или exec(), обрабатываем результат. Это делает API универсальным инструментом, который стоит освоить каждому JavaScript-разработчику.

Заключение

Мы прошли путь от базового синтаксиса до реальных сценариев использования. Давайте повторим ключевые выводы и обратим внимание на детали, которые часто упускают из виду.

Важное предостережение

Несмотря на все преимущества, у URLPattern есть важное ограничение, о котором следует знать, особенно при разработке серверных приложений. Использовать URLPattern для маршрутизации HTTP-запросов на высоконагруженном сервере может быть плохой идеей.

В основе URLPattern (как и библиотеки path-to-regexp, на которой он основан) лежит преобразование каждого шаблона в отдельное регулярное выражение. Это означает, что при поступлении запроса сервер должен последовательно проверять URL против регулярных выражений всех ваших маршрутов, пока не найдёт совпадение.

Проблема: этот подход создает "узкое место" производительности. Чем больше у вас маршрутов, тем медленнее обработка, особенно для запросов, которые не соответствуют ни одному маршруту (например, 404). В худшем случае приходится проверять все регулярные выражения.

Бенчмарки: Согласно тестам, опубликованным в феврале 2025 года, специализированный маршрутизатор на основе структуры данных "radix tree" (префиксное дерево), такой как find-my-way (используется в Fastify), может быть примерно в 100 раз быстрее URLPattern в сценариях со смешанными и динамическими маршрутами.

  • URLPattern в худшем случае обрабатывает около 6,900 запросов в секунду.
  • find-my-way в аналогичном тесте обрабатывает около 742,000 запросов в секунду.

mcollina/router-benchmark

Проблема безопасности (ReDoS): Регулярные выражения в основе URLPattern открывают не только проблемы производительности, но и потенциальные уязвимости. В сентябре 2024 года была обнаружена критическая уязвимость CVE-2024-45296 (CVSS 7.5) в библиотеке path-to-regexp, на которой основан URLPattern.

Уязвимость возникает при использовании двух параметров в одном сегменте пути с разделителем, не являющимся точкой (например, /:a-:b). Злоумышленник может отправить специально сформированный запрос, который из-за катастрофического бэктрекинга будет обрабатываться в тысячи раз дольше обычного, блокируя event loop и вызывая отказ в обслуживании (DoS).

Вывод: URLPattern был спроектирован для клиентской маршрутизации (например, в Service Worker или SPA), где количество маршрутов невелико, а производительность не так критична. Для production-сервера с десятками или сотнями маршрутов и высокой нагрузкой лучше использовать проверенные серверные решения, такие как встроенные роутеры фреймворков (Express, Fastify, Koa) или специализированные библиотеки, построенные на эффективных структурах данных.

Если вы всё же хотите использовать URLPattern на сервере (например, в небольших проектах или скриптах на Deno), следите за спецификацией URLPatternList, которая призвана решить эту проблему, но пока не реализована.

Важные нюансы

Регистрозависимость

По умолчанию URL Pattern API обрабатывает URL с учётом регистра символов. Это соответствует спецификации URL, но может отличаться от поведения некоторых JavaScript-фреймворков, которые традиционно используют регистронезависимое сравнение.

// По умолчанию регистр имеет значение
const pattern = new URLPattern('/Books/:id');
console.log(pattern.test('/books/123')); // false

// Регистронезависимый режим
const insensitivePattern = new URLPattern('/Books/:id', { ignoreCase: true });
console.log(insensitivePattern.test('/books/123')); // true

Опция ignoreCase применяется ко всему паттерну и влияет на все компоненты URL.

Нормализация паттернов

При создании паттерн автоматически нормализуется. Это значит, что синтаксически разные, но семантически эквивалентные выражения приводятся к единому виду:

// Эти паттерны эквивалентны после нормализации
const pattern1 = new URLPattern('/blog/{about}');
const pattern2 = new URLPattern('/blog/about');
// {about} без модификатора нормализуется в about

Нормализация также применяется к самим URL:

  • Unicode-символы в пути кодируются (percent-encoding)
  • Домены приводятся к punycode
  • Избыточные элементы пути (/foo/./bar/) сворачиваются (/foo/bar/)

Завершающие слэши

Важная особенность: завершающий слэш в пути не совпадает автоматически. Это частая причина неожиданного поведения:

const pattern = new URLPattern('/books');
console.log(pattern.test('/books')); // true
console.log(pattern.test('/books/')); // false

// Чтобы разрешить оба варианта:
const flexiblePattern = new URLPattern('/books{/}?');
console.log(flexiblePattern.test('/books')); // true
console.log(flexiblePattern.test('/books/')); // true

Производительность и лучшие практики

  1. Переиспользуйте паттерны. Создание URLPattern — операция не бесплатная, особенно если паттерн сложный. Создавайте экземпляры один раз и сохраняйте их.

  2. Используйте test() вместо exec() когда не нужны данные. Если вам нужно только проверить соответствие, test() работает быстрее, так как не строит объект результата.

  3. Осторожно с регулярными выражениями. Regex-группы удобны, но они сложнее в отладке и могут влиять на производительность. В некоторых окружениях (например, Service Worker) их использование может быть ограничено.

  4. Проверяйте поддержку. Несмотря на статус Baseline, всегда полезно проверить наличие API:

    if (typeof URLPattern !== 'undefined') {
    // Используем URL Pattern API
    } else {
    // Fallback на самописный парсинг или библиотеку
    }

Интеграция с существующими инструментами

path-to-regexp

Если вы уже используете библиотеку path-to-regexp (например, в Express или Next.js), переход на URL Pattern API будет практически безболезненным — синтаксис максимально близок.

TypeScript

API отлично типизируется. Вот пример утилиты для извлечения типов параметров из паттерна:

type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {};

// Использование
const pattern = new URLPattern('/users/:userId/posts/:postId');
type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// Params = { userId: string; postId: string; }

Итог

URL Pattern API — это пример того, как современная веб-платформа вбирает в себя лучшие практики, которые раньше существовали только в виде сторонних библиотек. Он предлагает единый, стандартизированный подход к работе с URL, который работает везде: от браузера до сервера.

Три причины начать использовать URL Pattern API уже сегодня:

  • Стандартизация. Единый синтаксис для клиента и сервера, понятный любому JavaScript-разработчику.
  • Производительность и область применения. Нативная реализация устраняет необходимость в дополнительных библиотеках для базовых задач. Однако важно понимать её ограничения: URLPattern отлично подходит для клиентской маршрутизации (SPA, Service Workers) и серверов с небольшим числом маршрутов. Для высоконагруженных серверов с десятками и сотнями эндпоинтов существуют более эффективные алгоритмы (например, на основе префиксных деревьев), которые могут быть в сотни раз быстрее. Всегда выбирайте инструмент под конкретную задачу.
  • Надежность. Встроенная нормализация и чёткие правила наследования избавляют от классов багов, связанных с ручным парсингом URL.

Попробуйте заменить в своём следующем проекте хотя бы один самописный парсер URL на URLPattern — и вы почувствуете разницу.

Полезные ссылки

Часто задаваемые вопросы (FAQ)

Можно ли использовать URL Pattern API в старых браузерах?

API доступен во всех современных браузерах с сентября 2025 года (статус Baseline). Для поддержки действительно старых браузеров (например, Internet Explorer) потребуется полифил. Существует несколько реализаций, например urlpattern-polyfill. Однако в 2026 году доля пользователей старых браузеров незначительна, и в большинстве проектов можно использовать API нативно.

Чем URL Pattern API отличается от path-to-regexp?

Синтаксис практически идентичен — создатели API ориентировались на эту библиотеку. Главное отличие: URL Pattern API — это встроенное решение, не требующее установки зависимостей. Оно работает быстрее за счёт нативной реализации и доступно одинаково и в браузере, и на сервере. Если вы уже используете path-to-regexp, переход на новый API будет почти безболезненным.

Почему мой паттерн не совпадает с URL, хотя визуально они одинаковы?

Самые частые причины:

  • Завершающий слэш. Паттерн /books не совпадёт с /books/. Используйте /books{/}? чтобы разрешить оба варианта.
  • Регистр символов. По умолчанию сравнение регистрозависимо. Добавьте { ignoreCase: true } при создании паттерна.
  • Неявная нормализация. URL Pattern автоматически нормализует URL (кодирует Unicode, сворачивает /./ и т.д.). Убедитесь, что ваш тестовый URL соответствует нормализованной форме.
Можно ли использовать регулярные выражения в Service Worker?

Это зависит от реализации браузера. Спецификация разрешает ограничивать использование regex-групп в некоторых окружениях из соображений производительности. Проверить, используются ли регулярные выражения в конкретном экземпляре URLPattern, можно через свойство hasRegExpGroups. Если вам нужна максимальная совместимость, старайтесь обходиться без regex.

Как получить все параметры из URL одним объектом?

Метод exec() возвращает результат с группами для каждого компонента URL отдельно. Вы можете собрать их в один объект:

const pattern = new URLPattern({
pathname: '/:entity/:id?',
search: '*'
});
const match = pattern.exec('https://example.com/products/123?color=red');
if (match) {
const allParams = {
...match.pathname.groups,
...match.search.groups
};
console.log(allParams); // { entity: 'products', id: '123', '0': 'color=red' }
}
Что означает статус «Baseline Newly Available»?

Это статус, который присваивается веб-технологиям, когда они начинают поддерживаться во всех основных браузерных движках (Chromium, Firefox, Safari). Для URL Pattern API это произошло в сентябре 2025 года. Статус означает, что технологию можно безопасно использовать в продакшене без опасений, что у части пользователей она не сработает.

Поддерживается ли API в Node.js?

Нативно — пока нет, но ведутся работы по включению его в ядро. Однако вы можете использовать полифил для Node.js. В Deno и Bun API доступен нативно.

Комментарии


Дополнительные материалы

Предыдущая Статья

Настройка доверенной публикации в npm: пошаговая инструкция