URL Encode JavaScript β€” encodeURIComponent()

Β·Front-end & Node.js DeveloperΒ·Reviewed bySophie LaurentΒ·Published

Use the free online URL Encode Online directly in your browser β€” no install required.

Try URL Encode Online Online β†’

When you build a search URL, pass a redirect path as a query parameter, or construct an OAuth authorization request, special characters like &, =, and spaces will silently corrupt the URL unless you URL encode them first. JavaScript offers three built-in approaches β€” encodeURIComponent(), encodeURI(), and URLSearchParamsβ€” each designed for a different use case, and choosing the wrong one is the root cause of most encoding bugs I've encountered in code reviews. For quick one-off encoding without any code, ToolDeck's URL Encoder handles it instantly in the browser. This guide covers all three approaches in depth (JavaScript ES2015+ / Node.js 10+): when to use each, how they differ for spaces and reserved characters, real-world Fetch and API patterns, CLI usage, and the four mistakes that cause the most subtle production bugs.

  • βœ“encodeURIComponent() is the right choice for encoding individual parameter values β€” it encodes every character except A–Z, a–z, 0–9, and - _ . ! ~ * ' ( )
  • βœ“encodeURI() encodes a complete URL while preserving structural characters (/ ? # & = :) β€” never use it for individual values
  • βœ“URLSearchParams auto-encodes key-value pairs using application/x-www-form-urlencoded format, where spaces become + instead of %20
  • βœ“Double-encoding is the most common production bug: encodeURIComponent(encodeURIComponent(value)) turns %20 into %2520
  • βœ“The qs library handles nested objects and arrays in query strings natively β€” stdlib URLSearchParams does not

What is URL Encoding?

URL encoding (formally called percent-encoding, defined in RFC 3986) converts characters that are not allowed or have special meaning in a URL into a safe representation. Each unsafe byte is replaced with a percent sign followed by two hexadecimal digits β€” the ASCII code of the character. A space becomes %20, an ampersand becomes %26, a forward slash becomes %2F.

The characters that are always safe and never encoded are called unreserved characters: letters A–Z and a–z, digits 0–9, and the four symbols - _ . ~. Everything else either must be encoded when used as data, or has a structural role in the URL itself (like / separating path segments or &separating query parameters). The practical consequence: a product name like β€œWireless Keyboard & Mouse” in a query parameter destroys the URL structure if passed raw.

Before Β· text
After Β· text
https://shop.example.com/search?q=Wireless Keyboard & Mouse&category=peripherals
https://shop.example.com/search?q=Wireless%20Keyboard%20%26%20Mouse&category=peripherals

encodeURIComponent() β€” The Right Function for Query Parameters

encodeURIComponent() is the workhorse of URL encoding in JavaScript. It encodes every character except the unreserved set (A–Z, a–z, 0–9, - _ . ! ~ * ' ( )). Critically, it encodes all characters that have structural meaning in URLs β€” &, =, ?, #, / β€” which makes it safe for use in parameter values. No import is needed; it is a global function available in all JavaScript environments.

Minimal working example

JavaScript (browser / Node.js)
// Encode a search query parameter that contains special characters
const searchQuery  = 'Wireless Keyboard & Mouse'
const filterStatus = 'in-stock'
const maxPrice     = '149.99'

const searchUrl = `https://shop.example.com/products?` +
  `q=${encodeURIComponent(searchQuery)}` +
  `&status=${encodeURIComponent(filterStatus)}` +
  `&maxPrice=${encodeURIComponent(maxPrice)}`

console.log(searchUrl)
// https://shop.example.com/products?q=Wireless%20Keyboard%20%26%20Mouse&status=in-stock&maxPrice=149.99

Encoding a redirect URL as a query parameter

JavaScript
// The redirect destination is itself a URL β€” it must be fully encoded
// or the outer URL parser will misinterpret its ? and & as its own
const redirectAfterLogin = 'https://dashboard.internal/reports?view=weekly&team=platform'

const loginUrl = `https://auth.company.com/login?next=${encodeURIComponent(redirectAfterLogin)}`

console.log(loginUrl)
// https://auth.company.com/login?next=https%3A%2F%2Fdashboard.internal%2Freports%3Fview%3Dweekly%26team%3Dplatform

// Decoding on the receiving end
const params    = new URLSearchParams(window.location.search)
const next      = params.get('next')              // Automatically decoded
const nextUrl   = new URL(next!)                  // Safe to parse
console.log(nextUrl.hostname)                     // dashboard.internal

Encoding non-ASCII and Unicode characters

JavaScript
// encodeURIComponent handles Unicode natively in all modern environments
// Each UTF-8 byte of the character is percent-encoded
const customerName = 'MΓΌller, Sophie'
const productTitle = '東京 wireless adapter'
const reviewText   = 'Muy bueno β€” funciona perfectamente'

console.log(encodeURIComponent(customerName))
// M%C3%BCller%2C%20Sophie

console.log(encodeURIComponent(productTitle))
// %E6%9D%B1%E4%BA%AC%20wireless%20adapter

console.log(encodeURIComponent(reviewText))
// Muy%20bueno%20%E2%80%94%20funciona%20perfectamente

// Decoding back
console.log(decodeURIComponent('M%C3%BCller%2C%20Sophie'))
// MΓΌller, Sophie
Note:encodeURIComponent()uses the JavaScript engine's internal UTF-16 string encoding, then encodes each UTF-8 byte of the character separately. A character like ΓΌ (U+00FC) encodes to %C3%BC because its UTF-8 representation is two bytes: 0xC3 and 0xBC. This is correct and follows the URI standard β€” servers are expected to decode the UTF-8 byte sequence back to the original codepoint.

JavaScript URL Encoding Functions β€” Character Reference

The three native encoding approaches differ in exactly which characters they encode. The table below shows the output for the most commonly problematic characters:

CharacterRole in URLencodeURIComponent()encodeURI()URLSearchParams
Spaceword separator%20%20+
&param separator%26kept%26
=key=value%3Dkept%3D
+encoded space (form)%2B%2B%2B
?query start%3Fkept%3F
#fragment%23kept%23
/path separator%2Fkept%2F
:scheme / port%3Akept%3A
@auth separator%40kept%40
%percent literal%25%25%25
~unreservedkeptkeptkept

The critical column is encodeURIComponent() vs encodeURI() for & and =: encodeURI() leaves them untouched, which is correct when encoding a full URL but catastrophic when encoding a value that contains these characters.

encodeURI() β€” When to Preserve URL Structure

encodeURI() is designed for encoding a complete URLβ€” it preserves all characters that are valid structural parts of a URI: the scheme (https://), the host, path separators, query delimiters, and the fragment identifier. Use it when you receive a URL that may contain spaces or non-ASCII characters in its path segments, but you need to keep its structure intact.

Sanitising a user-provided URL

JavaScript
// A URL pasted from a document with spaces in the path and non-ASCII chars
const rawUrl = 'https://cdn.example.com/assets/product images/MΓΌnchen chair.png'

const safeUrl = encodeURI(rawUrl)
console.log(safeUrl)
// https://cdn.example.com/assets/product%20images/M%C3%BCnchen%20chair.png

// encodeURIComponent would break it β€” it encodes the :// and all / characters
const broken = encodeURIComponent(rawUrl)
console.log(broken)
// https%3A%2F%2Fcdn.example.com%2Fassets%2Fproduct%20images%2FM%C3%BCnchen%20chair.png
// ↑ Not a valid URL β€” the scheme and slashes are destroyed
Note:The URL constructor is usually a better choice than encodeURI() for handling user-supplied URL strings β€” it normalises the URL, validates the structure, and gives you a clean API to access each component. Reserve encodeURI() for cases where you need a string result and already know the input is structurally a valid URL.

URLSearchParams β€” The Modern Approach for Query Strings

URLSearchParams is the idiomatic way to build and parse query strings in modern JavaScript. It is available globally in all modern browsers and Node.js 10+, and handles encoding automatically β€” you work with plain key-value pairs and it produces the correct output. One important detail: it follows the application/x-www-form-urlencoded specification, which encodes spaces as + rather than %20. This is correct and widely supported, but you should be aware of it when your server expects a specific format.

Building a search request URL

JavaScript (browser / Node.js 10+)
// Building a search API URL with multiple parameters
const filters = {
  query:    'standing desk',
  category: 'office-furniture',
  minPrice: '200',
  maxPrice: '800',
  inStock:  'true',
  sortBy:   'price_asc',
}

const params = new URLSearchParams(filters)

const apiUrl = `https://api.example.com/v2/products?${params}`
console.log(apiUrl)
// https://api.example.com/v2/products?query=standing+desk&category=office-furniture&minPrice=200&maxPrice=800&inStock=true&sortBy=price_asc

// Appending additional params after construction
params.append('page', '2')
params.append('tag', 'ergonomic & adjustable')
console.log(params.toString())
// query=standing+desk&...&tag=ergonomic+%26+adjustable

Parsing an incoming query string

JavaScript
// Both browser (window.location.search) and Node.js (req.url) scenarios
function parseWebhookCallbackUrl(rawSearch: string) {
  const params = new URLSearchParams(rawSearch)

  return {
    eventId:     params.get('event_id'),       // null if missing
    timestamp:   Number(params.get('ts')),
    signature:   params.get('sig'),
    redirectUrl: params.get('redirect'),       // Automatically decoded
    tags:        params.getAll('tag'),         // Handles repeated keys
  }
}

const callbackQuery = '?event_id=evt_9c2f&ts=1717200000&sig=sha256%3Dabc123&redirect=https%3A%2F%2Fdashboard.internal%2F&tag=payment&tag=webhook'

const parsed = parseWebhookCallbackUrl(callbackQuery)
console.log(parsed.redirectUrl)  // https://dashboard.internal/
console.log(parsed.tags)         // ['payment', 'webhook']

How to URL-Encode Parameters in JavaScript Fetch Requests

The most common place URL encoding appears in production code is inside fetch() calls β€” either building the request URL or sending form data in the request body. Each scenario has its own correct approach.

GET request β€” encoding query parameters

JavaScript (browser / Node.js 18+)
async function searchInventory(params: {
  sku?:      string
  warehouse: string
  minStock:  number
  updatedAfter?: Date
}): Promise<{ items: unknown[]; total: number }> {
  const searchParams = new URLSearchParams()

  if (params.sku)          searchParams.set('sku',          params.sku)
  searchParams.set('warehouse',  params.warehouse)
  searchParams.set('min_stock',  String(params.minStock))
  if (params.updatedAfter) searchParams.set('updated_after', params.updatedAfter.toISOString())

  const url = `https://inventory.internal/api/items?${searchParams}`

  const res = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${process.env.INVENTORY_API_KEY}`,
      'Accept':        'application/json',
    },
  })

  if (!res.ok) {
    throw new Error(`Inventory API ${res.status}: ${await res.text()}`)
  }

  return res.json()
}

const results = await searchInventory({
  warehouse:    'eu-west-1',
  minStock:     10,
  updatedAfter: new Date('2025-01-01'),
})

console.log(`Found ${results.total} items`)

POST request β€” URL-encoding a form body

JavaScript
// application/x-www-form-urlencoded POST β€” used by OAuth token endpoints,
// legacy form-submission APIs, and some webhook providers
async function requestOAuthToken(
  clientId:     string,
  clientSecret: string,
  code:         string,
  redirectUri:  string,
): Promise<{ access_token: string; expires_in: number }> {
  const body = new URLSearchParams({
    grant_type:    'authorization_code',
    client_id:     clientId,
    client_secret: clientSecret,
    code,
    redirect_uri:  redirectUri,  // URLSearchParams encodes this automatically
  })

  const res = await fetch('https://oauth.provider.com/token', {
    method:  'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body:    body.toString(),
    // body: body β€” also works directly in modern environments (Fetch spec accepts URLSearchParams)
  })

  if (!res.ok) {
    const err = await res.json()
    throw new Error(`OAuth error: ${err.error_description ?? err.error}`)
  }

  return res.json()
}

When you need to test or debug an encoded URL without setting up a script, paste the raw value directly into the URL Encoder β€” it encodes and decodes instantly, showing you exactly what the browser and server will see. Useful for inspecting OAuth redirect URIs, webhook callback URLs, and CDN signed request parameters.

Command-Line URL Encoding with Node.js and Shell

For shell scripts, CI pipelines, or quick debugging, several command-line approaches work without writing a full script.

bash
# ── Node.js one-liners β€” cross-platform (macOS, Linux, Windows) ────────

# encodeURIComponent β€” encode a single value
node -e "console.log(encodeURIComponent(process.argv[1]))" "Wireless Keyboard & Mouse"
# Wireless%20Keyboard%20%26%20Mouse

# Build a complete query string
node -e "console.log(new URLSearchParams(JSON.parse(process.argv[1])).toString())"   '{"q":"standing desk","category":"office-furniture","page":"1"}'
# q=standing+desk&category=office-furniture&page=1

# Decode a percent-encoded string
node -e "console.log(decodeURIComponent(process.argv[1]))" "Wireless%20Keyboard%20%26%20Mouse"
# Wireless Keyboard & Mouse

# ── curl β€” automatic encoding ───────────────────────────────────────────
# curl --data-urlencode encodes values automatically in GET and POST
curl -G "https://api.example.com/search"   --data-urlencode "q=Wireless Keyboard & Mouse"   --data-urlencode "category=office furniture"

# ── Python one-liner (available on most systems) ─────────────────────────
python3 -c "from urllib.parse import quote; print(quote(input(), safe=''))"
# (type the string and press Enter)

# ── jq + bash: encode every value in a JSON object ───────────────────────
echo '{"q":"hello world","tag":"node & express"}' |   node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));            console.log(new URLSearchParams(d).toString())"
# q=hello+world&tag=node+%26+express

High-Performance Alternative: qs

The built-in URLSearchParams does not support nested objects or arrays β€” the shape of query strings used by many APIs and frameworks. The qs library (30M+ weekly npm downloads) handles the full range of query string patterns used in the wild: nested objects with bracket notation (filters[status]=active), repeated keys, custom encoders, and configurable array serialisation formats.

bash
npm install qs
# or
pnpm add qs
JavaScript
import qs from 'qs'

// URLSearchParams cannot represent this structure natively
const reportFilters = {
  dateRange: {
    from: '2025-01-01',
    to:   '2025-03-31',
  },
  status:    ['published', 'archived'],
  author:    { id: 'usr_4f2a9c1b', role: 'editor' },
  workspace: 'ws-platform-eu',
}

// qs produces bracket-notation query strings used by Express, Rails, Django REST
const query = qs.stringify(reportFilters, { encode: true })
console.log(query)
// dateRange%5Bfrom%5D=2025-01-01&dateRange%5Bto%5D=2025-03-31&status%5B0%5D=published&status%5B1%5D=archived&author%5Bid%5D=usr_4f2a9c1b&author%5Brole%5D=editor&workspace=ws-platform-eu

// Human-readable (no encoding) β€” useful for debugging
console.log(qs.stringify(reportFilters, { encode: false }))
// dateRange[from]=2025-01-01&dateRange[to]=2025-03-31&status[0]=published&status[1]=archived...

// Parsing back
const parsed = qs.parse(query)
console.log(parsed.dateRange)   // { from: '2025-01-01', to: '2025-03-31' }
console.log(parsed.status)      // ['published', 'archived']

For flat key-value parameters, URLSearchParams is always the right choice β€” it is built-in, has zero overhead, and is universally supported. Reach for qs only when you need nested structures, array serialisation formats other than repeated keys, or you are integrating with a back-end framework that expects bracket-notation query strings.

Common Mistakes

I've seen these four mistakes repeatedly in production codebases β€” most are silent failures that only surface when a value contains a special character, which is exactly the kind of bug that slips through unit tests and only appears with real user data.

Mistake 1 β€” Using encodeURI() for query parameter values

Problem: encodeURI() does not encode &, =, or +. When a value contains these characters, they are interpreted as query string syntax, silently splitting or overwriting parameters. Fix: always use encodeURIComponent() for individual values.

Before Β· JavaScript
After Β· JavaScript
// ❌ encodeURI does not encode & and = in the value
//    "plan=pro&promo=SAVE20" is treated as two separate params
const planName = 'Pro Plan (save=20% & free trial)'
const url = `/checkout?plan=${encodeURI(planName)}`
// /checkout?plan=Pro%20Plan%20(save=20%%20&%20free%20trial)
//                                          ↑ & breaks the query string
// βœ… encodeURIComponent encodes & and = safely
const planName = 'Pro Plan (save=20% & free trial)'
const url = `/checkout?plan=${encodeURIComponent(planName)}`
// /checkout?plan=Pro%20Plan%20(save%3D20%25%20%26%20free%20trial)
//                                   ↑ = and & are both encoded

Mistake 2 β€” Double-encoding an already-encoded string

Problem: Calling encodeURIComponent() on a value that is already percent-encoded turns the % into %25, so %20 becomes %2520. The server decodes once and receives a literal %20 instead of a space. Fix: decode first, then re-encode, or ensure the value is never encoded twice.

Before Β· JavaScript
After Β· JavaScript
// ❌ encodedParam already contains %20 β€” encoding again produces %2520
const encodedParam  = 'Berlin%20Office'
const url = `/api/locations/${encodeURIComponent(encodedParam)}`
// /api/locations/Berlin%2520Office
// Server sees: "Berlin%20Office" (a literal percent-twenty, not a space)
// βœ… Decode first if the value may already be encoded
const maybeEncoded = 'Berlin%20Office'
const clean = decodeURIComponent(maybeEncoded)  // 'Berlin Office'
const url   = `/api/locations/${encodeURIComponent(clean)}`
// /api/locations/Berlin%20Office β€” correct

Mistake 3 β€” Not accounting for the + / %20 difference between URLSearchParams and encodeURIComponent

Problem: URLSearchParams encodes spaces as + (application/x-www-form-urlencoded), while encodeURIComponent() uses %20. Mixing both in the same URL β€” for example, appending a pre-encoded string to a URLSearchParams output β€” produces inconsistent encoding that confuses some parsers. Fix: pick one approach and use it consistently throughout a URL-building function.

Before Β· JavaScript
After Β· JavaScript
// ❌ Mixed: URLSearchParams uses + for spaces, but we're appending
//    a manually-encoded segment that uses %20
const base   = new URLSearchParams({ category: 'office furniture' })
const extra  = `sort=${encodeURIComponent('price asc')}`
const url    = `/api/products?${base}&${extra}`
// /api/products?category=office+furniture&sort=price%20asc
//              ↑ two different space encodings in the same URL
// βœ… Use URLSearchParams exclusively β€” consistent encoding throughout
const params = new URLSearchParams({
  category: 'office furniture',
  sort:     'price asc',
})
const url = `/api/products?${params}`
// /api/products?category=office+furniture&sort=price+asc

Mistake 4 β€” Forgetting to encode path segments that contain slashes

Problem: A resource identifier like a file path (reports/2025/q1.pdf) used as a REST path segment contains / characters that the server router interprets as path separators, routing to a non-existent endpoint. Fix: always use encodeURIComponent() for path segments that may contain slashes.

Before Β· JavaScript
After Β· JavaScript
// ❌ The file path contains / β€” the server receives a 3-segment path
//    instead of a single resource ID
const filePath = 'reports/2025/q1-financials.pdf'
const url      = `https://storage.example.com/objects/${filePath}`
// https://storage.example.com/objects/reports/2025/q1-financials.pdf
// β†’ Server routes to /objects/:year/:filename β€” 404 or wrong resource
// βœ… encodeURIComponent encodes / as %2F β€” single path segment
const filePath = 'reports/2025/q1-financials.pdf'
const url      = `https://storage.example.com/objects/${encodeURIComponent(filePath)}`
// https://storage.example.com/objects/reports%2F2025%2Fq1-financials.pdf
// β†’ Server receives the full file path as one resource identifier

JavaScript URL Encoding Methods β€” Quick Comparison

MethodEncodes spaces asEncodes & = ?Encodes # / :Use caseRequires install
encodeURIComponent()%20βœ… yesβœ… yesEncoding individual parameter valuesNo
encodeURI()%20❌ no❌ noSanitising a complete URL stringNo
URLSearchParams+βœ… yesβœ… yesBuilding and parsing query stringsNo
URL constructorauto by componentautoautoConstructing and normalising full URLsNo
qs library%20 (configurable)βœ… yesβœ… yesNested objects and arrays in query stringsnpm install qs

For the vast majority of cases, the choice reduces to three scenarios. Use URLSearchParams when constructing multi-parameter query strings from structured data β€” it is the safest and most readable option. Use encodeURIComponent() for encoding a single value in a template literal URL, for path segments, or for values in systems that expect %20 instead of + for spaces (such as AWS S3 signed URLs). Reach for qs only when your query string carries nested objects or arrays that URLSearchParams cannot represent natively.

Frequently Asked Questions

What is the difference between encodeURIComponent() and encodeURI() in JavaScript?
encodeURIComponent() encodes every character except the unreserved set (A–Z, a–z, 0–9, - _ . ! ~ * ' ( )), including structural characters like & = ? # / :. It is designed for encoding individual query parameter values or path segments. encodeURI() preserves structural URL characters β€” it does not encode & = ? # / : @ β€” because it is designed to sanitise a complete URL without breaking its structure. Using encodeURI() on a value that contains & or = is a common source of silent data corruption.
Why does URLSearchParams encode spaces as + instead of %20?
URLSearchParams follows the application/x-www-form-urlencoded specification (originally derived from HTML form submissions), which encodes spaces as + rather than %20. Both + and %20 are valid representations of a space in query strings, and most servers accept both. However, some APIs β€” particularly AWS Signature v4, Google APIs, and custom REST backends β€” require %20. For those cases, use encodeURIComponent() to build the query string manually, or call params.toString().replace(/+/g, '%20') on the URLSearchParams output.
How do I URL-encode a path segment that contains forward slashes in JavaScript?
Use encodeURIComponent() β€” it encodes / as %2F, turning the entire path into a single segment. encodeURI() does not encode /, so it would be treated as a real path separator by the router. This matters for REST APIs that use resource identifiers in path position: file paths (reports/2025/q1.pdf), S3 object keys, and composite IDs (org/team/member). After encoding: encodeURIComponent('reports/2025/q1.pdf') β†’ 'reports%2F2025%2Fq1.pdf'.
How do I decode a percent-encoded URL parameter in JavaScript?
Use decodeURIComponent() for individual parameter values, or let URLSearchParams handle it automatically. When you do new URLSearchParams(window.location.search).get('key'), the value is already decoded β€” you do not need to call decodeURIComponent() on it again. Call decodeURIComponent() directly only when you receive the raw encoded string outside of URLSearchParams, for example from a custom header or from a URL fragment parsed manually. Note: decodeURIComponent() throws a URIError on malformed sequences like a bare % character β€” wrap it in a try/catch if the input comes from user data.
Can I use URLSearchParams to encode nested objects like { filters: { status: "active" } }?
No. URLSearchParams only supports flat key-value pairs. It serialises a nested object by calling .toString() on each value, which produces "[object Object]" β€” silently incorrect. For nested structures, use the qs library: qs.stringify({ filters: { status: 'active' } }) produces filters%5Bstatus%5D=active (bracket notation), which is understood by Express, Rails, and Django REST Framework. Alternatively, serialise nested data as a JSON string and pass it as a single parameter value: params.set('filters', JSON.stringify({ status: 'active' })).
Does the Fetch API automatically URL-encode query parameters?
No. fetch() accepts a URL string and passes it verbatim to the network layer β€” it does not parse or encode query parameters. If you concatenate unencoded values into the URL string, special characters will corrupt the request. The correct pattern is to build the URL with URLSearchParams or encodeURIComponent() before passing it to fetch(): const url = new URL('/api/search', base); url.searchParams.set('q', userInput); const res = await fetch(url). The URL constructor and URLSearchParams together give you safe automatic encoding with a clean API.

For a one-click encode or decode without writing any code, paste your string directly into the URL Encoder β€” it handles percent-encoding and decoding instantly in your browser, with the encoded output ready to copy into a fetch call or terminal.

AC
Alex ChenFront-end & Node.js Developer

Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.

SL
Sophie LaurentTechnical Reviewer

Sophie is a full-stack developer focused on TypeScript across the entire stack β€” from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.