Skip to main content

Workers URL Shortener

A URL shortener running on Cloudflare Workers with KV storage.

Location: examples/workers-url-shortener

Features​

  • Cloudflare Workers deployment
  • KV storage for persistence
  • URL validation
  • Hit counter for analytics

Running​

cd examples/workers-url-shortener
pnpm install
pnpm dev

Key Code​

Worker entry point (src/worker.ts):

import { onCloudflareWorkers } from '@zeltjs/adapter-cloudflare-workers';

import { app } from './app';

const cfApp = await onCloudflareWorkers(app);

export default {
  fetch: cfApp.http.fetch,
};

Controller with KV access (src/url/url.controller.ts):

import { Controller, Get, HTTPException, inject, Post, request, response } from '@zeltjs/core';
import * as v from 'valibot';

import { KVService } from './kv.service';
import type { UrlRecord } from './types';

const ShortenBody = v.object({
  url: v.pipe(v.string(), v.url()),
});

const generateCode = (): string => {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
  let code = '';
  for (let i = 0; i < 6; i++) {
    code += chars[Math.floor(Math.random() * chars.length)];
  }
  return code;
};

@Controller('/')
export class UrlController {
  constructor(private kv = inject(KVService)) {}

  @Post('/shorten')
  async shorten(req = request(ShortenBody), res = response()) {
    const body = await req.body();
    const code = generateCode();
    const record: UrlRecord = {
      url: body.url,
      createdAt: Date.now(),
      hits: 0,
    };
    await this.kv.set(code, record);
    return res.json({ code, shortUrl: `/${code}` }, 201);
  }

  @Get('/stats/:code')
  async stats(req = request()) {
    const code = req.pathParam('code');
    const record = await this.kv.get(code);
    if (!record) {
      throw new HTTPException(404, { message: 'URL not found' });
    }
    return { code, url: record.url, hits: record.hits, createdAt: record.createdAt };
  }

  @Get('/:code')
  async redirect(req = request(), res = response()) {
    const code = req.pathParam('code');
    const record = await this.kv.get(code);
    if (!record) {
      throw new HTTPException(404, { message: 'URL not found' });
    }
    await this.kv.incrementHits(code);
    return res.redirect(record.url, 302);
  }
}

KV service (src/url/kv.service.ts):

import { CloudflareBindings } from '@zeltjs/adapter-cloudflare-workers';
import { Injectable, inject } from '@zeltjs/core';

import type { UrlRecord } from './types';

@Injectable()
export class KVService {
  constructor(private readonly bindings = inject(CloudflareBindings)) {}

  async get(code: string): Promise<UrlRecord | null> {
    return this.bindings.get('URLS').get<UrlRecord>(`url:${code}`, 'json');
  }

  async set(code: string, record: UrlRecord): Promise<void> {
    await this.bindings.get('URLS').put(`url:${code}`, JSON.stringify(record));
  }

  async incrementHits(code: string): Promise<void> {
    const record = await this.get(code);
    if (record) {
      record.hits += 1;
      await this.set(code, record);
    }
  }
}

API Endpoints​

MethodPathDescription
POST/shortenCreate a short URL
GET/:codeRedirect to original URL
GET/stats/:codeGet URL statistics

wrangler.toml​

name = "url-shortener"
main = "src/worker.ts"
compatibility_date = "2024-01-01"

[[kv_namespaces]]
binding = "URLS"
id = "your-kv-namespace-id"