{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreicvbbzvbcielskykuqppam66bd5kca424s4q33kbg5iyfmr3w3a6a",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mowozoczv3e2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreigofw2wwpyaoxjap5j2d6mlsv66j3lwznplo6n5jq6upfg6sbk22a"
},
"mimeType": "image/webp",
"size": 101522
},
"path": "/vbv_rai/i-built-my-own-url-shortener-because-bitly-charges-for-custom-aliases-2ank",
"publishedAt": "2026-06-23T05:31:58.000Z",
"site": "https://dev.to",
"tags": [
"node",
"javascript",
"webdev",
"showdev",
"urlzap.me",
"github.com/vbv0507/url-shortener",
"Monitorly"
],
"textContent": "# I Built My Own URL Shortener Because Bit.ly Charges for Custom Aliases\n\n**TL;DR:** Bit.ly puts custom short links behind a paywall. So I built URLzap — a production-ready URL shortener with custom aliases, expiry, QR codes, click analytics, and JWT auth. It's live, free to use, and fully open source.\n\n## The Problem\n\nI was sharing links to my side projects — Monitorly, my portfolio, GitHub repos — and wanted clean, memorable short URLs like `urlzap.me/pulsewatch` instead of a random string of characters.\n\nBit.ly's answer? **Pay for a premium plan.**\n\nI'm a B.Tech student. I'm not paying $35/month to shorten my own links. So I did what any developer would do — I spent a weekend building it myself.\n\n## What is URLzap?\n\nURLzap is a full-featured URL shortener built with **Node.js, Express.js, MongoDB, and Azure**. It's not just a redirect service — it has features I actually use daily.\n\n**Live:** urlzap.me\n**GitHub:** github.com/vbv0507/url-shortener\n\n## Features\n\n### 1. Custom Aliases\n\nThe whole reason I built this. Instead of `urlzap.me/xK92mP`, I can create `urlzap.me/pulsewatch` for my Monitorly project. Clean, memorable, and free.\n\n### 2. Link Expiry\n\nSet an expiry date on any link. After that date, the link auto-deactivates. Useful for time-limited campaign links or sharing temporary access.\n\n### 3. QR Code Generation\n\nEvery short link automatically generates a QR code. Handy for sharing links in presentations, README files, or anywhere you want a scannable format.\n\n### 4. Click Analytics Dashboard\n\nA per-link dashboard showing total clicks over time. You can see which links are getting traffic and when. Simple but genuinely useful for tracking your own projects.\n\n### 5. JWT Authentication\n\nAll endpoints are secured with JWT. Each user only sees and manages their own links — no leaking between accounts.\n\n### 6. Rate Limiting + Input Validation\n\nBackend-level rate limiting to prevent abuse, and full input validation on all endpoints. Not an afterthought — built in from the start.\n\n## Tech Stack\n\nLayer | Technology\n---|---\nRuntime | Node.js\nFramework | Express.js\nDatabase | MongoDB + Mongoose\nAuth | JWT (jsonwebtoken)\nQR Codes | qrcode npm package\nDeployment | Azure App Service\nCI/CD | GitHub Actions\n\n## How It Works — The Core Flow\n\n### Shortening a URL\n\n\n // POST /api/links\n // Creates a short link with optional custom alias and expiry\n\n router.post('/', authenticate, async (req, res) => {\n const { originalUrl, customAlias, expiresAt } = req.body;\n\n // Check if alias is already taken\n const existing = await Link.findOne({ shortCode: customAlias });\n if (existing) return res.status(409).json({ error: 'Alias already taken' });\n\n const shortCode = customAlias || generateRandomCode(6);\n\n const link = await Link.create({\n originalUrl,\n shortCode,\n userId: req.user.id,\n expiresAt: expiresAt || null,\n clicks: 0\n });\n\n res.json({ shortUrl: `https://urlzap.me/${shortCode}`, link });\n });\n\n\n### Redirecting + Tracking Clicks\n\n\n // GET /:shortCode\n // Looks up the link, checks expiry, redirects, and logs the click\n\n router.get('/:shortCode', async (req, res) => {\n const link = await Link.findOne({ shortCode: req.params.shortCode });\n\n if (!link) return res.status(404).send('Link not found');\n\n // Check expiry\n if (link.expiresAt && new Date() > link.expiresAt) {\n return res.status(410).send('This link has expired');\n }\n\n // Log click and redirect\n await Link.findByIdAndUpdate(link._id, { $inc: { clicks: 1 } });\n res.redirect(link.originalUrl);\n });\n\n\n### QR Code Generation\n\n\n // GET /api/links/:id/qr\n // Returns a QR code as a PNG data URL\n\n const QRCode = require('qrcode');\n\n router.get('/:id/qr', authenticate, async (req, res) => {\n const link = await Link.findById(req.params.id);\n const qrDataUrl = await QRCode.toDataURL(`https://urlzap.me/${link.shortCode}`);\n res.json({ qr: qrDataUrl });\n });\n\n\n## MongoDB Schema\n\n\n const linkSchema = new mongoose.Schema({\n originalUrl: { type: String, required: true },\n shortCode: { type: String, required: true, unique: true },\n userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },\n clicks: { type: Number, default: 0 },\n expiresAt: { type: Date, default: null },\n createdAt: { type: Date, default: Date.now }\n });\n\n // Index for fast lookups on every redirect\n linkSchema.index({ shortCode: 1 });\n\n\nThe `shortCode` index is important — every redirect hits this lookup, so it needs to be fast even at scale.\n\n## Deployment on Azure\n\nThe app runs on **Azure App Service** (Central India region) with **GitHub Actions CI/CD**. Every push to `main` triggers an automatic deploy — no manual steps.\n\n\n\n # .github/workflows/deploy.yml (simplified)\n - name: Deploy to Azure Web App\n uses: azure/webapps-deploy@v2\n with:\n app-name: 'urlzap'\n publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}\n package: .\n\n\nThis means I can push a fix and it's live in under 2 minutes.\n\n## What I Learned Building This\n\n**1. Indexes matter more than you think.**\nBefore adding the `shortCode` index, redirects were doing a full collection scan. After adding it, lookup time dropped dramatically. Always index fields you query frequently.\n\n**2. Input validation is not optional.**\nI got lazy on validation early on and had to go back and add it everywhere. Build it in from day one — middleware-based validation keeps it clean.\n\n**3. Expiry logic is tricky at scale.**\nI handle expiry on every redirect request right now. At scale, a background cron job that deactivates expired links periodically is a better pattern.\n\n**4. Rate limiting saves you from yourself.**\nWithout rate limiting, a single script could hammer your redirect endpoint and spike your Azure costs. Add it early.\n\n## What's Next\n\n * [ ] Custom domain support (bring your own domain)\n * [ ] Link groups / folders for organisation\n * [ ] API access with API keys (not just JWT)\n * [ ] Detailed analytics (referrer, country, device)\n\n\n\n## Try It / Use the Code\n\n**Live:** urlzap.me\n**GitHub:** github.com/vbv0507/url-shortener\n\nIf you're tired of paying for custom short links, fork it, self-host it, or just use URLzap directly.\n\nHappy to answer questions about any part of the build in the comments.\n\n_Also building: Monitorly — an API uptime monitoring platform. Check it out if you want to monitor your own endpoints._\n\n**Tags:** `#node` `#javascript` `#webdev` `#showdev`",
"title": "I Built My Own URL Shortener Because Bit.ly Charges for Custom Aliases"
}