{
  "$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"
}