
Long URLs are not always a problem.
Sometimes they are exactly what you want.
A campaign URL like this contains useful information:
https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=august_launch&utm_content=cta_button
The problem starts when the same URL has to move through several parts of a workflow:
- an email system
- a notification service
- a CMS
- a QR code
- an internal dashboard
- a social post
- a database
- an analytics pipeline
At that point, manually creating, copying, checking, and shortening URLs becomes repetitive.
Instead of treating URL shortening as a separate manual step, we can build a small workflow around it.
In this post, we'll create a practical JavaScript flow that looks like this:
Destination URL
↓
Add UTM parameters
↓
Validate the URL
↓
Shorten it
↓
Store both URLs
↓
Use the short URL for distribution
The important part is not the URL shortener itself.
The important part is keeping the original tracking URL correct while making the distribution URL easier to handle.
1. Build the UTM URL programmatically
You could concatenate strings manually:
const url =
"https://example.com/pricing" +
"?utm_source=newsletter" +
"&utm_medium=email" +
"&utm_campaign=august_launch";
It works, but it becomes easy to introduce encoding mistakes as the number of parameters grows.
The URL and URLSearchParams APIs give us a cleaner option.
function buildCampaignUrl(destination, campaign) {
const url = new URL(destination);
url.searchParams.set("utm_source", campaign.source);
url.searchParams.set("utm_medium", campaign.medium);
url.searchParams.set("utm_campaign", campaign.name);
if (campaign.content) {
url.searchParams.set("utm_content", campaign.content);
}
if (campaign.term) {
url.searchParams.set("utm_term", campaign.term);
}
return url.toString();
}
Now we can create a campaign URL like this:
const longUrl = buildCampaignUrl(
"https://example.com/pricing",
{
source: "newsletter",
medium: "email",
name: "august_launch",
content: "cta_button",
}
);
console.log(longUrl);
The output will contain the complete tracking URL.
This approach has another useful property: values are encoded correctly by the URL API instead of relying on manual string manipulation.
2. Validate before shortening
A short URL should not hide a broken long URL.
Before sending anything to a shortening service, validate the destination.
A basic validator can look like this:
function isHttpUrl(value) {
try {
const url = new URL(value);
return (
url.protocol === "http:" ||
url.protocol === "https:"
);
} catch {
return false;
}
}
Then:
if (!isHttpUrl(longUrl)) {
throw new Error("Invalid campaign URL");
}
This only validates the structure.
It does not guarantee that the destination actually exists.
If your workflow is important enough, you may also want a separate check for the destination before distributing the link.
3. Shorten the complete URL, not half of it
One mistake that makes tracking URLs harder to reason about is splitting the workflow like this:
Destination
↓
Shorten URL
↓
Add tracking parameters somewhere later
For a typical campaign workflow, I prefer this:
Destination
↓
Add UTM parameters
↓
Validate
↓
Shorten the COMPLETE URL
That gives you one canonical campaign URL before the shortening layer is introduced.
For example:
https://example.com/pricing
becomes:
https://example.com/pricing?utm_source=newsletter&utm_medium=email&utm_campaign=august_launch
and that complete URL becomes the input to the shortener.
This makes the system easier to debug later.
If something goes wrong, you still have the exact campaign destination that was originally created.
4. Put the shortener behind a function
Your application should not need to know the details of the shortening service everywhere.
Instead, isolate it.
async function shortenUrl(longUrl) {
// call your URL shortening service here
}
Then the rest of your application only needs to understand:
const shortUrl = await shortenUrl(longUrl);
This small abstraction is useful if you ever:
- change providers
- add retries
- add logging
- introduce caching
- disable shortening temporarily
- use different providers in different environments
The campaign creation logic stays independent from the shortener implementation.
5. Example using a URL shortening API
As one concrete example, nly.kr provides a URL shortening API.
Its current documented endpoint is:
POST https://nly.kr/api/shorten
The API accepts the original URL and a required category, and API-key authentication can be sent using the X-API-Key header.
For developer-related links, dev is one of the documented categories.
A Node.js helper could look like this:
async function shortenUrl(longUrl) {
const apiKey = process.env.NLY_API_KEY;
if (!apiKey) {
throw new Error("NLY_API_KEY is not configured");
}
const response = await fetch(
"https://nly.kr/api/shorten",
{
method: "POST",
headers: {
"Content-Type":
"application/x-www-form-urlencoded",
"X-API-Key": apiKey,
Accept: "application/json",
},
body: new URLSearchParams({
url: longUrl,
category: "dev",
}),
}
);
const data = await response.json();
if (!response.ok) {
throw new Error(
data.message ||
`Shortening failed: HTTP ${response.status}`
);
}
return data.short_url;
}
Notice that the API key comes from:
process.env.NLY_API_KEY
rather than being hardcoded.
Do not put a private API key into browser-side JavaScript.
If authentication is required, make the request from your backend.
You can check the current endpoint, authentication options, parameters, and response format in the nly.kr URL Shortener API documentation.
6. Combine the pieces
Now we can turn the separate steps into one function.
async function createCampaignLink({
destination,
source,
medium,
campaign,
content,
}) {
const longUrl = buildCampaignUrl(
destination,
{
source,
medium,
name: campaign,
content,
}
);
if (!isHttpUrl(longUrl)) {
throw new Error("Invalid campaign URL");
}
const shortUrl = await shortenUrl(longUrl);
return {
originalUrl: destination,
campaignUrl: longUrl,
shortUrl,
};
}
Usage:
const result = await createCampaignLink({
destination: "https://example.com/pricing",
source: "newsletter",
medium: "email",
campaign: "august_launch",
content: "cta_button",
});
console.log(result);
Conceptually, the result looks like this:
{
originalUrl: "...",
campaignUrl: "...?utm_source=...",
shortUrl: "..."
}
Now we have three separate pieces of information instead of throwing the original URL away.
7. Store the long URL too
This is probably the most important operational detail.
Do not store only:
short_url
Store enough information to reconstruct what happened.
For example:
{
id: 127,
campaign: "august_launch",
channel: "newsletter",
original_url: "https://example.com/pricing",
campaign_url: "https://example.com/pricing?...",
short_url: "...",
created_at: "..."
}
Why?
Because three months later, a short URL alone tells you very little.
You may want to answer:
- Which campaign created this link?
- Which destination did it point to?
- Which UTM parameters were used?
- Was a new short URL generated later?
- Which channel received it?
Keeping the original campaign URL makes debugging and reporting much easier.
8. Avoid generating duplicates unnecessarily
Imagine your application runs this code every time someone opens an admin page:
const shortUrl = await shortenUrl(longUrl);
You could end up generating multiple short links for exactly the same campaign URL.
A better workflow may be:
Create campaign URL
↓
Search existing record
↓
Found?
├─ Yes → reuse saved short URL
└─ No → create short URL and save it
A simple pseudo implementation:
async function getOrCreateShortUrl(longUrl) {
const existing = await findByCampaignUrl(longUrl);
if (existing) {
return existing.shortUrl;
}
const shortUrl = await shortenUrl(longUrl);
await saveLink({
campaignUrl: longUrl,
shortUrl,
});
return shortUrl;
}
This also reduces unnecessary external API calls.
9. Treat shortening as optional infrastructure
Your main product probably should not stop working just because a URL shortening API is temporarily unavailable.
Consider this flow:
Create content
↓
Generate campaign URL
↓
Try shortening
↓
Success → use short URL
Failure → keep long URL / retry later
Depending on your use case, falling back to the original URL may be better than failing the entire operation.
For example:
async function safeShortenUrl(longUrl) {
try {
return await shortenUrl(longUrl);
} catch (error) {
console.error(
"URL shortening failed:",
error
);
return longUrl;
}
}
Whether this fallback makes sense depends on your application.
For a marketing batch that must use predefined short links, you might prefer to stop the job instead.
For a non-critical notification, using the original URL may be acceptable.
The key is to decide deliberately.
10. Add timeouts and retries carefully
External APIs can fail.
Your application should expect:
- network errors
- authentication errors
- bad input
- server errors
- timeouts
Retries are useful for temporary failures, but do not blindly retry every error.
For example:
400 Bad Request
usually suggests that changing nothing and retrying the same request will not help.
A temporary server or network error may be different.
A production workflow could use:
Request
↓
Success → save result
Temporary failure
↓
retry with backoff
Permanent/input failure
↓
log + require correction
This matters even for a feature as small as URL shortening.
11. Keep API keys on the server
If your shortener requires authentication, avoid this:
// DON'T do this in browser code
const API_KEY = "my-secret-key";
Everything delivered to the browser should be considered visible to the user.
A safer architecture is:
Browser
↓
Your backend
↓
URL Shortener API
Your backend reads the key from an environment variable or secret manager.
For example:
NLY_API_KEY=...
and:
const apiKey =
process.env.NLY_API_KEY;
The browser never needs to receive the credential.
12. A more realistic campaign workflow
Once everything is combined, an internal system might work like this:
Editor creates campaign
↓
Destination selected
↓
UTM parameters generated
↓
Campaign URL validated
↓
Existing short URL lookup
↓
Short URL generated if needed
↓
Both URLs stored
↓
Short URL sent to email/SMS/QR workflow
This is much more reliable than manually moving URLs between browser tabs.
It also creates a clean boundary between different concerns:
Tracking
= UTM parameters
Distribution
= short URL
Analytics
= campaign/traffic data
Storage
= mapping between them
Keeping those responsibilities separate makes the workflow easier to maintain.
What I would implement first
If I were adding this to a small project, I would not start with a complicated link-management system.
I'd begin with four pieces:
1. buildCampaignUrl()
2. isHttpUrl()
3. shortenUrl()
4. save the long + short URL pair
Then add caching, retries, dashboards, or batch processing only when the project actually needs them.
A URL shortener is a small component.
The useful part is the workflow around it.
When you create the full campaign URL first, validate it, shorten it behind a small abstraction, and keep the original URL in storage, long UTM links become much easier to manage without losing the information that made them useful in the first place.
Top comments (0)