Shortening a URL in JavaScript is one HTTP POST. You send the long link to a shortener's API with fetch, pass your API key as a Bearer token, and read the short link out of the JSON response. Since Node 18 there is no package to install - fetch is a global.
The catch is where that code runs. A shortener API key is a spending credential, and client-side JavaScript is public, so the browser is out. Everything below runs server side: a Node script, a route handler, a serverless function.
This is the JavaScript version of the general how-to-shorten-a-url walkthrough, which covers the dashboard and the browser flow instead. If you have not picked a service yet, the free URL shortener API overview lays out the request shape, the auth model, and the free-tier limits this article assumes. Field names below are Elido's, but the shape - POST a destination, get a short URL back - ports to most modern shorteners.
The Fastest Way: One fetch Call in Node
Put the key in an environment variable and POST the destination to the links endpoint:
const res = await fetch("https://api.elido.app/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
destination_url: "https://example.com/spring-sale?utm_source=newsletter",
}),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`shorten failed: ${res.status}`);
const { short_url } = await res.json();
console.log(short_url); // -> https://s.elido.me/ab12cd
Run it with node --env-file=.env shorten.js and the key never touches the source.
That if (!res.ok) line is not decoration. fetch rejects only on a network failure or an abort, so a 401 or a 500 comes back as a perfectly resolved promise with ok: false. Skip the check and a revoked key looks exactly like a success until something downstream tries to use undefined as a link. Anyone arriving from axios gets bitten by this once.
AbortSignal.timeout is the other habit worth forming. Without it a hung connection blocks until the socket dies on its own, which in a cron job means the job is simply gone.
Why the Browser Is the Wrong Place for This
Paste that snippet into a React component and two things break. The first is CORS: a key-authenticated API does not send Access-Control-Allow-Origin for arbitrary sites, so the browser blocks the response. Reading the MDN CORS reference usually sends people hunting for a proxy that makes the error go away.
The error is the symptom. The real problem is the second one: a key in front-end code is readable by anyone who opens DevTools or greps the bundle. Bundlers do not hide it, NEXT_PUBLIC_ prefixes advertise it, and a public key on a paid API is somebody else's free quota.
The shape that works puts a route of yours in the middle. The browser posts a plain URL to your own endpoint, your server adds the key, and the key stays in an environment variable the client never sees:
// app/api/shorten/route.js (Next.js App Router)
export async function POST(request) {
const { url } = await request.json();
const res = await fetch("https://api.elido.app/v1/links", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ destination_url: url }),
});
const { short_url } = await res.json();
return Response.json({ short_url });
}
Validate url before forwarding it. An open endpoint that shortens anything anyone posts is an open redirect with extra steps, and the open redirect write-up covers what attackers do with one.
Add a Timeout, Retries, and an Idempotency Key
The happy path is eight lines. A job that runs unattended needs to survive a 429, a transient 5xx, and the nastier case where the POST succeeds but the response never arrives: your code times out, retries, and now there are two short links for one destination.
An idempotency key closes that gap. Generate one per URL, reuse it across every retry of that URL, and the API returns the original link instead of minting a second one.
import { randomUUID } from "node:crypto";
const API = "https://api.elido.app/v1/links";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function shorten(destinationUrl, { retries = 3 } = {}) {
const idempotencyKey = randomUUID(); // one per URL, not per attempt
for (let attempt = 0; attempt < retries; attempt++) {
const res = await fetch(API, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ELIDO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ destination_url: destinationUrl }),
signal: AbortSignal.timeout(10_000),
});
if (res.status === 429) {
await sleep(Number(res.headers.get("Retry-After") ?? 2) * 1000);
continue;
}
if (res.status >= 500) {
await sleep(2 ** attempt * 1000); // exponential backoff
continue;
}
if (!res.ok) {
throw new Error(`shorten failed: ${res.status} ${await res.text()}`);
}
return (await res.json()).short_url;
}
throw new Error(`shorten failed after ${retries} attempts`);
}
A 401 or 403 will never fix itself, so those throw immediately rather than burning three attempts. A 429 waits exactly as long as the Retry-After header asks. Only 5xx gets exponential backoff. The rate limits and idempotency deep-dive explains why a naive retry loop is how one vendor outage turns into two.
Want to run the snippets as written? Create a key on the free plan, export it as ELIDO_API_KEY, and every example on this page works unchanged.
Shorten URLs in Bulk Without Tripping the Rate Limit
The obvious bulk version is Promise.all(urls.map(shorten)). It fires every request in the same tick, so a list of 500 URLs becomes 500 simultaneous connections and the API answers most of them with 429. A small pool fixes it: a fixed number of workers pulling from a shared queue, so concurrency stays flat no matter how long the list is.
export async function shortenAll(urls, concurrency = 8) {
const queue = [...urls];
const results = new Map();
const worker = async () => {
while (queue.length) {
const url = queue.pop();
try {
results.set(url, await shorten(url));
} catch (err) {
results.set(url, `ERROR: ${err.message}`); // one bad URL must not sink the batch
}
}
};
await Promise.all(Array.from({ length: concurrency }, worker));
return results;
}
Keying results by the original URL means a partial failure is visible and re-runnable instead of a silent gap in the output. Set concurrency to whatever the plan documents; eight is a safe starting point, and the response headers tell you when to come down.
If you would rather not maintain any of this, the API and SDKs ship the retry and pagination logic already wired, and the SDK quickstart shows the typed version of the same call.
Where the Code Should Live
Four homes cover almost everything. A one-off script that shortens a CSV runs fine as plain Node with --env-file. A form on your site needs the route handler above, where the key sits on the server and the browser talks only to you. A queue worker suits a batch big enough that no HTTP request should wait for it. And a build step that generates campaign links belongs in CI, with the key as a repository secret.
One caveat for edge runtimes: node:crypto is not available there, but crypto.randomUUID() is a Web Crypto global on Vercel Edge, Cloudflare Workers, and Deno, so swap the import and the rest of the code is unchanged.
The home changes, the checklist does not: key in the environment, a timeout on every request, res.ok read before the body, and an idempotency key on anything that can run twice. Teams doing this at scale usually end up reading what else the API exposes - clicks, tags, expiry - and wiring webhooks for link events rather than polling.
Read the Cornerstone Series
This sits in the engineering cluster. Start with the free URL shortener API guide for the endpoint shape and auth, then the rate limits and idempotency piece for behaving well under load. The live reference is the API docs, and URL shorteners for developers covers what to look for in an API before you commit to one.
Related on the Blog
- How to shorten a URL in Python with the requests library
- How to shorten a URL in PHP with curl, Guzzle, and WordPress
- How to shorten a URL in Go with net/http
- Free URL shortener API: the request shape, auth, and limits
- How to shorten a URL in Java with the built-in HttpClient
- How to shorten a URL in C# with HttpClient and .NET
Preguntas frecuentes
How do I shorten a URL in JavaScript?
Send an HTTP POST to a URL shortener's API with fetch, passing the long URL in the JSON body and your API key as a Bearer token, then read the short link out of the parsed response. On Node 18 or newer fetch is built in, so no package install is needed: build the headers, post the destination URL to the links endpoint, and use the short_url field of the JSON you get back.
Can I shorten a URL in the browser with JavaScript?
Not with your API key in the page. Anything in client-side JavaScript is readable in DevTools, so a key shipped to the browser is a public key that anyone can spend your quota with. Call the shortener from a server route or serverless function instead and have the browser call that route.
Why does my fetch request to a URL shortener return a CORS error?
Because the shortener's API does not send an Access-Control-Allow-Origin header for your site, which is deliberate on a key-authenticated endpoint. The browser blocks the response even though the request may have succeeded. Moving the call to the server side removes the browser from the path and the error with it.
Do I need an npm package to shorten URLs in Node.js?
No. Node ships a global fetch from version 18 onward, and a shortener API call is one POST with two headers, so a dependency buys you very little. Reach for an SDK when you want typed responses, built-in retries, and pagination helpers across many endpoints rather than a single call.
How do I shorten many URLs at once in Node.js?
Run a small pool of workers instead of Promise.all over the whole list, so only a handful of requests are in flight at any moment and you stay under the rate limit. Send an Idempotency-Key per URL so a retried request returns the original link instead of creating a duplicate.
Does fetch throw an error on a 401 or 500 response?
No, and this trips people up coming from axios. fetch only rejects on a network failure or an aborted request, so a 401 or 500 arrives as a resolved response with ok set to false. Check response.ok yourself before reading the body, or a failed call will look like a successful one.
Prueba Elido
Pega una URL, obtén un enlace corto
Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.
Gratis, sin registro · 2 por día