# node:url

O módulo `node:url` fornece os utilitários do Node.js para resolver, analisar e formatar URLs. Com ele, uma function lê as partes de uma URL de forma estruturada, altera essas partes e constrói URLs para roteamento e requisições de API. O Azion Runtime oferece o módulo por meio da compatibilidade com Node.js. Uma function pode usá-lo para analisar a URL da requisição recebida, ler os parâmetros de query dela e construir URLs ao fazer proxy ou redirecionar tráfego. No Azion Runtime, `url.URL` é o mesmo objeto que o global `URL`, e as funções legadas `url.parse()`, `url.format()` e `url.resolve()` funcionam em uma function após o deploy.

---

## Exemplos

Cada exemplo é uma function completa que importa `url` de `node:url`. A resposta abaixo de cada exemplo é a que uma function com deploy feito retorna.

### Análise e formatação de URL

Esta function cria uma URL com a classe global `URL`, constrói uma string de URL a partir de um objeto com `url.format()` e analisa essa string com `url.parse()`:

```javascript
/**
 * An example of using Node.js URL API in an Azion Function.
 * Support:
 * - Partially supported (Extended by library `url`)
 * @module runtime-apis/nodejs/url/main
 * @example
 * // Build and run with the Azion CLI:
 * azion build
 * azion dev
 */
import url from "node:url";
/**
 * An example of using the Node.js URL API in an Azion Function.
 * @param {*} event
 * @returns {Promise<Response>}
 */
const main = async (event) => {
  /**
   * URL globalThis object
   * https://developer.mozilla.org/en-US/docs/Web/API/URL
   * if use URL in the browser, don't need to import
   */
  const newUrl = new URL("https://example.com/some/path?format=json&page=1");
  console.log("newURL", newUrl);

  const urlFormated = url.format({
    protocol: "https",
    hostname: "example.com",
    pathname: "/some/path",
    query: {
      page: 1,
      format: "json",
    },
  });

  console.log(url.parse(urlFormated));

  return new Response("Done!", { status: 200 });
};

export default main;
```

A function responde com `Done!`. Ela registra no log a URL, o aviso de API obsoleta que `url.parse()` exibe e o objeto analisado:

```text
newURL "https://example.com/some/path?format=json&page=1"
[DeprecationWarning] [unenv] [node:url] DEP0169: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
{"auth":null,"hash":null,"host":"example.com","hostname":"example.com","href":"https://example.com/some/path?page=1&format=json","path":"/some/path?page=1&format=json","pathname":"/some/path","protocol":"https:","search":"?page=1&format=json","slashes":true,"port":null,"query":"page=1&format=json"}
```

### Análise da URL da requisição

Esta function lê os componentes da URL da requisição, reúne os parâmetros de query dela e divide o caminho em segmentos:

```javascript
// url module imported for legacy API demonstration
// new URL() and URLSearchParams are available globally
import url from "node:url";

const main = async (event) => {
  const request = event.request;
  const requestUrl = new URL(request.url);

  // Extract URL components
  const urlInfo = {
    href: requestUrl.href,
    origin: requestUrl.origin,
    protocol: requestUrl.protocol,
    hostname: requestUrl.hostname,
    port: requestUrl.port,
    pathname: requestUrl.pathname,
    search: requestUrl.search,
    hash: requestUrl.hash
  };

  // Parse query parameters
  const searchParams = requestUrl.searchParams;
  const queryParams = {};
  for (const [key, value] of searchParams) {
    queryParams[key] = value;
  }

  // ⚠️ url.parse() is deprecated since Node.js v11.0.0
  // Prefer the WHATWG URL API (new URL()) shown above
  const parsedUrl = url.parse(request.url, true);
  console.log("Parsed URL:", parsedUrl);

  // Get path segments
  const segments = requestUrl.pathname.split("/").filter(Boolean);

  return new Response(JSON.stringify({
    urlInfo,
    queryParams,
    segments,
    parsedQuery: parsedUrl.query
  }), {
    headers: { "Content-Type": "application/json" }
  });
};

export default main;
```

Para uma requisição `GET` para `https://example.com/blog/post-1`, a function responde com os componentes da URL, uma query vazia e os dois segmentos do caminho:

```json
{"urlInfo":{"href":"https://example.com/blog/post-1","origin":"https://example.com","protocol":"https:","hostname":"example.com","port":"","pathname":"/blog/post-1","search":"","hash":""},"queryParams":{},"segments":["blog","post-1"],"parsedQuery":{}}
```

### Construção dinâmica de URLs

Esta function constrói uma URL de API com parâmetros de query, uma URL de redirecionamento com `url.format()` e uma URL a partir das partes dela. Ela também resolve um caminho relativo duas vezes, com `url.resolve()` e com o construtor `URL`:

```javascript
import url from "node:url";

const main = async (event) => {
  // Build API URL with query parameters
  const apiUrl = new URL("https://api.example.com/v1/users");
  apiUrl.searchParams.set("page", "1");
  apiUrl.searchParams.set("limit", "10");
  apiUrl.searchParams.set("sort", "name");
  apiUrl.searchParams.set("active", "true");

  console.log("API URL:", apiUrl.toString());

  // Build redirect URL
  const redirectUrl = url.format({
    protocol: "https",
    hostname: "app.example.com",
    pathname: "/dashboard",
    query: {
      ref: "login",
      session: "abc123"
    }
  });

  // Resolve relative URL
  // ⚠️ url.resolve() is deprecated since Node.js v11.0.0
  // Prefer: new URL(relativePath, baseUrl).href
  const baseUrl = "https://example.com/docs/";
  const relativePath = "../api/reference";
  const resolvedUrl = url.resolve(baseUrl, relativePath);
  console.log("Resolved URL (deprecated):", resolvedUrl);

  // Modern alternative using URL constructor
  const modernResolvedUrl = new URL(relativePath, baseUrl).href;
  console.log("Resolved URL (modern):", modernResolvedUrl);

  // Build URL from parts
  const parts = {
    protocol: "https:",
    hostname: "cdn.example.com",
    port: "443",
    pathname: "/assets/images/logo.png"
  };
  const fullUrl = url.format(parts);

  return new Response(JSON.stringify({
    apiUrl: apiUrl.toString(),
    redirectUrl,
    resolvedUrl,
    builtUrl: fullUrl
  }), {
    headers: { "Content-Type": "application/json" }
  });
};

export default main;
```

A function responde com as quatro URLs. As duas formas de resolver o caminho relativo registram no log `https://example.com/api/reference`, e a URL construída a partir das partes mantém a porta `443`:

```json
{"apiUrl":"https://api.example.com/v1/users?page=1&limit=10&sort=name&active=true","redirectUrl":"https://app.example.com/dashboard?ref=login&session=abc123","resolvedUrl":"https://example.com/api/reference","builtUrl":"https://cdn.example.com:443/assets/images/logo.png"}
```

### Manipulação de parâmetros de query

Esta function lê, verifica, altera e ordena os parâmetros de query da requisição e, em seguida, constrói uma segunda query string a partir de um objeto:

```javascript
import url from "node:url";

const main = async (event) => {
  const requestUrl = new URL(event.request.url);
  const params = requestUrl.searchParams;

  // Read parameters
  const page = params.get("page") || "1";
  const limit = params.get("limit") || "10";
  const sort = params.get("sort");

  console.log("Page:", page, "Limit:", limit, "Sort:", sort);

  // Check if parameter exists
  const hasFilter = params.has("filter");
  console.log("Has filter:", hasFilter);

  // Get all values for a parameter (multi-value)
  const tags = params.getAll("tag");
  console.log("Tags:", tags);

  // Modify parameters
  params.set("page", "2");
  params.set("timestamp", Date.now().toString());
  params.delete("debug");

  // Iterate over parameters
  const allParams = [];
  for (const [key, value] of params) {
    allParams.push({ key, value });
  }

  // Sort parameters alphabetically
  params.sort();

  // Convert to string
  const queryString = params.toString();

  // Create new URLSearchParams from object
  const newParams = new URLSearchParams({
    format: "json",
    version: "2",
    pretty: "true"
  });

  return new Response(JSON.stringify({
    originalParams: { page, limit, sort },
    hasFilter,
    tags,
    modifiedParams: allParams,
    queryString,
    newParamsString: newParams.toString()
  }), {
    headers: { "Content-Type": "application/json" }
  });
};

export default main;
```

Para uma requisição sem query string, a function responde com os valores padrão, os parâmetros alterados e as duas query strings. O valor de `timestamp` vem de `Date.now()`:

```json
{"originalParams":{"page":"1","limit":"10","sort":null},"hasFilter":false,"tags":[],"modifiedParams":[{"key":"page","value":"2"},{"key":"timestamp","value":"1767268800000"}],"queryString":"page=2&timestamp=1767268800000","newParamsString":"format=json&version=2&pretty=true"}
```

### Validação e normalização de URL

Esta function verifica uma lista de URLs com o construtor `URL` e, em seguida, normaliza cada URL válida: ela converte o hostname para minúsculas e remove uma porta padrão:

```javascript
import url from "node:url";

const main = async (event) => {
  const request = event.request;
  // Note: In a real application, you might receive URLs from the request body
  // const { urls: customUrls } = await request.json().catch(() => ({}));

  // URLs to validate
  const testUrls = [
    "https://example.com/path",
    "http://localhost:3000/api?query=test",
    "HTTPS://EXAMPLE.COM/UPPERCASE",
    "https://example.com:443/standard-port",
    "https://user:pass@example.com/auth",
    "not-a-url",
    "ftp://files.example.com"
  ];

  const results = testUrls.map(testUrl => {
    try {
      const parsed = new URL(testUrl);
      return {
        original: testUrl,
        valid: true,
        normalized: parsed.href,
        protocol: parsed.protocol,
        hostname: parsed.hostname,
        isHttps: parsed.protocol === "https:"
      };
    } catch (error) {
      return {
        original: testUrl,
        valid: false,
        error: error.message
      };
    }
  });

  // Normalize a URL (lowercase hostname, default port removal)
  const normalizeUrl = (urlString) => {
    try {
      const parsed = new URL(urlString);
      parsed.hostname = parsed.hostname.toLowerCase();
      
      // Remove default ports
      if (parsed.protocol === "https:" && parsed.port === "443") {
        parsed.port = "";
      }
      if (parsed.protocol === "http:" && parsed.port === "80") {
        parsed.port = "";
      }
      
      return parsed.href;
    } catch {
      return null;
    }
  };

  const normalizedUrls = testUrls.map(normalizeUrl);

  return new Response(JSON.stringify({
    validationResults: results,
    normalizedUrls
  }), {
    headers: { "Content-Type": "application/json" }
  });
};

export default main;
```

A function responde com o resultado de cada URL e com a lista normalizada. O construtor `URL` rejeita `not-a-url` com a mensagem `Invalid URL: 'not-a-url'`, e a lista normalizada contém `null` no lugar dela:

```json
{"validationResults":[{"original":"https://example.com/path","valid":true,"normalized":"https://example.com/path","protocol":"https:","hostname":"example.com","isHttps":true},{"original":"http://localhost:3000/api?query=test","valid":true,"normalized":"http://localhost:3000/api?query=test","protocol":"http:","hostname":"localhost","isHttps":false},{"original":"HTTPS://EXAMPLE.COM/UPPERCASE","valid":true,"normalized":"https://example.com/UPPERCASE","protocol":"https:","hostname":"example.com","isHttps":true},{"original":"https://example.com:443/standard-port","valid":true,"normalized":"https://example.com/standard-port","protocol":"https:","hostname":"example.com","isHttps":true},{"original":"https://user:pass@example.com/auth","valid":true,"normalized":"https://user:pass@example.com/auth","protocol":"https:","hostname":"example.com","isHttps":true},{"original":"not-a-url","valid":false,"error":"Invalid URL: 'not-a-url'"},{"original":"ftp://files.example.com","valid":true,"normalized":"ftp://files.example.com/","protocol":"ftp:","hostname":"files.example.com","isHttps":false}],"normalizedUrls":["https://example.com/path","http://localhost:3000/api?query=test","https://example.com/UPPERCASE","https://example.com/standard-port","https://user:pass@example.com/auth",null,"ftp://files.example.com/"]}
```

### Roteamento de URL e correspondência de caminhos

Esta function compara o caminho da requisição com uma tabela de rotas, incluindo uma rota com um parâmetro `:id`, e verifica se a rota aceita o método da requisição. Ela responde `404` quando nenhuma rota corresponde e `405` quando uma rota corresponde, mas os métodos dela não incluem o método da requisição:

```javascript
import url from "node:url";

const main = async (event) => {
  const requestUrl = new URL(event.request.url);
  const pathname = requestUrl.pathname;

  // Define routes
  const routes = {
    "/": { handler: "home", methods: ["GET"] },
    "/api/users": { handler: "users", methods: ["GET", "POST"] },
    "/api/users/:id": { handler: "userDetail", methods: ["GET", "PUT", "DELETE"] },
    "/api/products": { handler: "products", methods: ["GET"] },
    "/health": { handler: "health", methods: ["GET"] }
  };

  // Match route
  const matchRoute = (path) => {
    // Exact match
    if (routes[path]) {
      return { ...routes[path], params: {} };
    }

    // Pattern match for :id style routes
    for (const [pattern, config] of Object.entries(routes)) {
      if (pattern.includes(":")) {
        const regex = new RegExp("^" + pattern.replace(/:\w+/g, "([^/]+)") + "$");
        const match = path.match(regex);
        if (match) {
          const paramNames = pattern.match(/:\w+/g) || [];
          const params = {};
          paramNames.forEach((name, i) => {
            params[name.slice(1)] = match[i + 1];
          });
          return { ...config, params };
        }
      }
    }

    return null;
  };

  const matchedRoute = matchRoute(pathname);
  const method = event.request.method;

  // Check if method is allowed
  const methodAllowed = matchedRoute?.methods?.includes(method);

  // Build response
  const response = {
    pathname,
    method,
    matched: matchedRoute ? {
      handler: matchedRoute.handler,
      params: matchedRoute.params,
      methodAllowed
    } : null,
    query: Object.fromEntries(requestUrl.searchParams)
  };

  return new Response(JSON.stringify(response, null, 2), {
    status: matchedRoute && methodAllowed ? 200 : matchedRoute ? 405 : 404,
    headers: { "Content-Type": "application/json" }
  });
};

export default main;
```

Para uma requisição `GET` para `https://example.com/blog/post-1`, um caminho que a tabela de rotas não lista, a function segue o ramo sem correspondência e responde com o status `404` e este corpo:

```json
{
  "pathname": "/blog/post-1",
  "method": "GET",
  "matched": null,
  "query": {}
}
```

---

## APIs com suporte

A tabela lista o status de cada API de `node:url` no Azion Runtime:

| API                      | Status                                                   |
| ------------------------ | -------------------------------------------------------- |
| `URL` (global)           | 🟢 Com suporte                                           |
| `URLSearchParams`        | 🟢 Com suporte                                           |
| `url.URL`                | 🟢 Com suporte                                           |
| `url.URLSearchParams`    | 🟢 Com suporte                                           |
| `url.format()`           | 🟢 Com suporte                                           |
| `url.parse()`            | 🟡 Com suporte (obsoleta, use `new URL()`)               |
| `url.resolve()`          | 🟡 Com suporte (obsoleta, use `new URL(relative, base)`) |
| `url.domainToASCII()`    | 🟡 Suporte parcial                                       |
| `url.domainToUnicode()`  | 🟡 Suporte parcial                                       |
| `url.fileURLToPath()`    | 🟡 Suporte parcial                                       |
| `url.pathToFileURL()`    | 🟡 Suporte parcial                                       |
| `url.urlToHttpOptions()` | 🟡 Suporte parcial                                       |

As APIs marcadas como 🟡 Suporte parcial têm funcionalidade limitada em comparação com a implementação completa do Node.js. Em uma function após o deploy, `url.fileURLToPath("file:///tmp/x.txt")` retorna `/tmp/x.txt`, `url.pathToFileURL("/tmp/x.txt")` retorna `file:///tmp/x.txt` e `url.domainToASCII("español.com")` retorna `xn--espaol-zwa.com`.

Uma function que chama `url.parse()` pode registrar no log este aviso de API obsoleta, como mostra o primeiro exemplo:

```text
[DeprecationWarning] [unenv] [node:url] DEP0169: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
```

O Node.js marca `url.parse()`, `url.resolve()` e `url.format()` como obsoletas a partir da v11.0.0. Use as APIs WHATWG `URL` e `URLSearchParams` em vez delas: no Azion Runtime, as duas são globais e não precisam de import.

---

## Recursos relacionados

- [APIs do Node.js](/pt-br/documentacao/devtools/runtime/node.md): Cada módulo do Node.js que o Azion Runtime resolve e o status de `url` entre eles.
- [Use APIs do Node.js com polyfills](/pt-br/documentacao/guias/desenvolvimento-de-aplicacoes/functions-e-runtime/use-polyfills.md): Como o build resolve `node:url` e os outros imports do Node.js para o Azion Runtime.
- [Documentação de url do Node.js](https://nodejs.org/api/url.html): A referência do Node.js para cada API de `node:url` que a tabela lista.
- [Documentação da API URL no MDN](https://developer.mozilla.org/en-US/docs/Web/API/URL): A referência Web da classe global `URL` que todos os exemplos usam.
