{"id":15408345,"url":"https://github.com/yusukebe/url-shortener","last_synced_at":"2025-09-19T07:32:21.008Z","repository":{"id":227029624,"uuid":"770232879","full_name":"yusukebe/url-shortener","owner":"yusukebe","description":"Creating URL Shortener with Cloudflare Pages","archived":false,"fork":false,"pushed_at":"2024-07-12T13:09:21.000Z","size":41,"stargazers_count":144,"open_issues_count":0,"forks_count":12,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-05T01:09:20.018Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yusukebe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-11T07:28:57.000Z","updated_at":"2024-12-02T16:35:26.000Z","dependencies_parsed_at":"2024-10-21T13:09:28.727Z","dependency_job_id":null,"html_url":"https://github.com/yusukebe/url-shortener","commit_stats":{"total_commits":17,"total_committers":3,"mean_commits":5.666666666666667,"dds":"0.11764705882352944","last_synced_commit":"23fbf3772f3857f11cbb023742da2d270390af2b"},"previous_names":["yusukebe/url-shortener"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukebe%2Furl-shortener","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukebe%2Furl-shortener/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukebe%2Furl-shortener/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yusukebe%2Furl-shortener/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yusukebe","download_url":"https://codeload.github.com/yusukebe/url-shortener/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233556712,"owners_count":18693772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-01T16:33:34.228Z","updated_at":"2025-09-19T07:32:15.714Z","avatar_url":"https://github.com/yusukebe.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# Creating URL Shortener with Cloudflare Pages\n\nLet's create a super simple URL Shortener with Cloudflare Pages!\nBy creating this application you will experience:\n\n- Creating web pages with Hono.\n- Using Cloudflare KV in your application.\n- Deploying your application to Cloudflare Pages.\n\n## The application feature\n\n- Developing with Vite.\n- Having UI.\n- The main code is less than 100 lines.\n- Validation with Zod.\n- Handling validation error.\n- CSRF Protection.\n\n## Demo\n\n![Demo](https://github.com/yusukebe/url-shortener/assets/10682/aab18332-b38e-4425-a5f8-e25b71fa9168)\n\n## Source Code\n\nYou can see the entire source code here.\n\n## Tutorial\n\nI'll show you how to create your application!\n\n---\n\n## Account\n\nTo deploy an application to Cloudflare Pages, a Cloudflare account is needed. Since it can be used within the free tier, if you don't have an account, please create one.\n\n## Project Setup\n\nLet's start by setting up the project.\n\n### Initial Project\n\nWe'll use a CLI called \"_create-hono_\" to create the project. Execute the following command:\n\n```txt\nnpm create hono@latest url-shortener\n```\n\nWhen prompted to choose a template, select \"**_cloudflare-pages_**\". Then, when asked about installing dependencies and which package manager to use, press Enter to proceed.\n\nNow, you have your initial project setup. Enter the project directory:\n\n```txt\ncd url-shortener\n```\n\n### Start the Development Server\n\nLet's start the development server. It's easy, just run the following command:\n\n```txt\nnpm run dev\n```\n\nBy default, it launches at `http://localhost:5173`, so access it. You should be able to see the page.\n\n### Create KV\n\nThis app uses Cloudflare KV, a Key-Value store. To use it, you need to create a KV project by running the following command:\n\n```txt\nnpm exec wrangler kv namespace create KV\n```\n\nYou'll see a message like this:\n\n```txt\n🌀 Creating namespace with title \"url-shortener-KV\"\n✨ Success!\nAdd the following to your configuration file in your kv_namespaces array:\n{ binding = \"KV\", id = \"xxxxxx\" }\n```\n\nCopy the `id` value `xxxxxx`, and write it into `wrangler.toml` in the format shown above.\n\n### Install Dependencies\n\nFor this app, we'll validate input values. For that, we'll include the Zod library and Hono middleware.\n\n```txt\nnpm i zod @hono/zod-validator\n```\n\n### Remove `public`\n\nFinally, the starter template includes a `public` directory with CSS for customization, but since we won't use it this time, let's remove it.\n\n```txt\nrm -rf public\n```\n\n## Writing Code\n\nNow, let's start coding.\n\n### Organize Layout\n\nWe'll arrange a common layout for the pages by editing `src/renderer.tsx`.\n\nTo save time, we'll use a CSS framework called [new.css](https://newcss.net/), which is a _class-less_ framework. This means you don't need to specify any special `class` values; the existing HTML styles will automatically look good.\n\nThe final version will look like this:\n\n```tsx\nimport { jsxRenderer } from 'hono/jsx-renderer'\n\nexport const renderer = jsxRenderer(({ children }) =\u003e {\n  return (\n    \u003chtml\u003e\n      \u003chead\u003e\n        \u003clink rel=\"stylesheet\" href=\"https://fonts.xz.style/serve/inter.css\" /\u003e\n        \u003clink rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css\"\u003e\u003c/link\u003e\n      \u003c/head\u003e\n      \u003cbody\u003e\n        \u003cheader\u003e\n          \u003ch1\u003e\n            \u003ca href=\"/\"\u003eURL Shortener\u003c/a\u003e\n          \u003c/h1\u003e\n        \u003c/header\u003e\n        \u003cdiv\u003e{children}\u003c/div\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  )\n})\n```\n\n### Making the Home Page\n\nFirst, we're making a home page. It responds when someone visits the root path `/`. Here's how we set it up.\n\n```ts\napp.get('/', (c) =\u003e {\n  //...\n})\n```\n\nInside the handler, we use `c.render()` to return HTML with our layout applied. We've set it up to send a POST request to `/create` to make a short URL.  Edit `src/index.tsx` with this:\n\n```tsx\napp.get('/', (c) =\u003e {\n  return c.render(\n    \u003cdiv\u003e\n      \u003ch2\u003eCreate shortened URL!\u003c/h2\u003e\n      \u003cform action=\"/create\" method=\"post\"\u003e\n        \u003cinput\n          type=\"text\"\n          name=\"url\"\n          autocomplete=\"off\"\n          style={{\n            width: '80%'\n          }}\n        /\u003e\n        \u0026nbsp;\n        \u003cbutton type=\"submit\"\u003eCreate\u003c/button\u003e\n      \u003c/form\u003e\n    \u003c/div\u003e\n  )\n})\n```\n\nThis will look something like this:\n\n![Screenshot](https://github.com/yusukebe/url-shortener/assets/10682/64bf3e39-8792-49e9-95ef-1f0bb813f2a8)\n\n### Making a Validator\n\nWe want to check the form data from the top page. So, let's make a validator.\n\nFirst, we import stuff from the library we installed earlier.\n\n```ts\nimport { z } from 'zod'\nimport { zValidator } from '@hono/zod-validator'\n```\n\nThen, we make a schema. This is how we say, \"We want a string that's a URL named `url`\".\n\n```ts\nconst schema = z.object({\n  url: z.string().url()\n})\n```\n\nWe register this with `zValidator`. The `form` we pass as the first argument is because we want to handle form requests.\n\n```ts\nconst validator = zValidator('form', schema)\n```\n\nLet's make an endpoint to handle the POST request to `/create` using our finished validator. Since a validators is middleware, we can put it before our handler. Then, we use `c.req.valid()` to get the value, which in this case, is named `url`.\n\n```ts\napp.post('/create', validator, async (c) =\u003e {\n  const { url } = c.req.valid('form')\n\n  // TODO: Create a short URL\n})\n```\n\nIf it passes the check, the value will be in `url`.\n\n### Defining KV Types\n\nNow that we have the form value, let's write the logic to make a short URL.\n\nFirst, we define the types for KV we're using. `KVNamespace` represents KV. In Hono, if you pass `Bindings` as a name for Cloudflare's Bindings type to the Hono class generics, you can then access `c.env.KV` with types.\n\n```ts\ntype Bindings = {\n  KV: KVNamespace\n}\n\nconst app = new Hono\u003c{\n  Bindings: Bindings\n}\u003e()\n```\n\n### Generating and Saving Keys\n\nLet's make a function named `createKey()` to generate keys for short URLs. We need the KV object and the URL to generate a key.\n\n```ts\napp.post('/create', validator, async (c) =\u003e {\n  const { url } = c.req.valid('form')\n\n  const key = await createKey(c.env.KV, url)\n\n  // ...\n})\n```\n\nThere are a few strategies for generating an unique key, but we'll go with this:\n\n- Create a random string.\n- Use 6 characters of it.\n- If there's no object in KV with that key, save the URL as its value.\n- If there is, run `createKey()` again.\n- Return the created key.\n\nYou can get and set values in KV with `kv.get(key)` and `kv.put(key, value)`.\n\nThe finished function looks like this:\n\n```ts\nconst createKey = async (kv: KVNamespace, url: string) =\u003e {\n  const uuid = crypto.randomUUID()\n  const key = uuid.substring(0, 6)\n  const result = await kv.get(key)\n  if (!result) {\n    await kv.put(key, url)\n  } else {\n    return await createKey(kv, url)\n  }\n  return key\n}\n```\n\n### Showing the Result\n\nNow we've made a key. The URL with this key as the pathname is our short URL. If you're developing locally and your key was, for example, `abcdef`, it would be:\n\n```txt\nhttp://localhost:5173/abcdef\n```\n\nWe made a page to display this URL in an `input` element for easy copying, using `autofocus` too.\n\n```tsx\napp.post('/create', validator, async (c) =\u003e {\n  const { url } = c.req.valid('form')\n  const key = await createKey(c.env.KV, url)\n\n  const shortenUrl = new URL(`/${key}`, c.req.url)\n\n  return c.render(\n    \u003cdiv\u003e\n      \u003ch2\u003eCreated!\u003c/h2\u003e\n      \u003cinput\n        type=\"text\"\n        value={shortenUrl.toString()}\n        style={{\n          width: '80%'\n        }}\n        autofocus\n      /\u003e\n    \u003c/div\u003e\n  )\n})\n```\n\nNow, the short URL is created and displayed nicely.\n\n![Screenshot](https://github.com/yusukebe/url-shortener/assets/10682/2eda47a4-8adb-460a-8432-289a00a36779)\n\n### Redirecting\n\nNow that we can generate short URLs, let's make them redirect to the registered URL. We use regex to match the address like `/abcdef` and, in the handler, get the value from KV using that string as the key. If it exists, that's the original URL, and we redirect there. If not, we go back to the top page.\n\n```ts\napp.get('/:key{[0-9a-z]{6}}', async (c) =\u003e {\n  the key = c.req.param('key')\n  const url = await c.env.KV.get(key)\n\n  if (url === null) {\n    return c.redirect('/')\n  }\n\n  return c.redirect(url)\n})\n```\n\n### Handling Errors\n\nWe're almost done, and it's looking good!\n\nBut one issue is what happens if someone puts a non-URL value in the form. The validator catches the error, but it just shows a string of JSON.\n\n![Screenshot](https://github.com/yusukebe/url-shortener/assets/10682/c8d809eb-4374-40d0-9745-b00a020410b3)\n\nLet's show an error page instead. For this, we write a hook as the third argument to `zValidator`. `result` is the result object from Zod validation, so we use it to decide what to do based on whether it was successful.\n\n```tsx\nconst validator = zValidator('form', schema, (result, c) =\u003e {\n  if (!result.success) {\n    return c.render(\n      \u003cdiv\u003e\n        \u003ch2\u003eError!\u003c/h2\u003e\n        \u003ca href=\"/\"\u003eBack to top\u003c/a\u003e\n      \u003c/div\u003e\n    )\n  }\n})\n```\n\nNow, if there's a validation error, an error message is shown.\n\n![Screenshot](https://github.com/yusukebe/url-shortener/assets/10682/e8c5e93a-feaa-4c61-b54a-b786ee9e83c2)\n\n#### Adding a CSRF Protector\n\nThis is the last step! Our URL shortening service is pretty great as it is, but there's a chance someone could send a POST request directly from a form on a different site. So, we use Hono's built-in middleware, [CSRF Protector](https://hono.dev/middleware/builtin/csrf).\n\nIt's super easy to use. Just import it.\n\n```ts\nimport { csrf } from 'hono/csrf'\n```\n\nAnd use it before the handler on routes where you want it.\n\n```ts\napp.post('/create', csrf(), validator, async (c) =\u003e {\n  const { url } = c.req.valid('form')\n  const key = await createKey(c.env.KV, url)\n  //...\n})\n```\n\nAnd that's it! You've made a URL shortening app with a UI, validation, error handling, and CSRF protection, all within about 100 lines in `index.tsx`!\n\n## Deploying\n\nLet's deploy to Cloudflare Pages. Run the following command:\n\n```txt\nnpm run deploy\n```\n\nIf it's your first time, you'll be asked a few questions like this. Just answer them:\n\n```txt\nCreate a new project\n? Enter the name of your new project: › url-shortener\n```\n\nAfter running the command, a URL for your deployed site will be displayed. It might look something like this:\n\n```txt\nhttps://random-strings.url-shortener-abc.pages.dev/\n```\n\nIt takes a bit of time to be ready for viewing after it's created, so let's wait. In some cases, you might be able to view it by accessing `url-shortener-abc.pages.dev`, removing the initial host name part.\n\n### Setting KV in the Dashboard\n\nBut wait! You might see an \"_Internal Server Error_\". This is because KV settings are not done for the production environment. Despite writing settings in `wrangler.toml`, they won't apply; dashboard settings are required. Go to the settings page of the Pages project you created, navigate to the KV section, and specify the namespace you created earlier with the name KV.\n\n![Screenshot](https://github.com/yusukebe/url-shortener/assets/10682/592b16a9-b124-4bf4-852f-c523aea0b249)\n\nDeploying again should work now!\n\n## Deleting the Project\n\nIf you're not planning to use it, remember to delete the production Pages project.\n\n## Summary\n\nWe made a URL shortening app using Cloudflare KV and Hono and deployed it to Cloudflare Pages. The main `src/index.tsx` is about 100 lines, but it's a complete app with page layouts, validation, and error handling, not just \"returning JSON\". However, as it stands, external users could potentially create unlimited short URLs, hitting KV indefinitely, so consider this for further development.\n\nHow was it? Pretty neat, right? Creating apps on Cloudflare Pages with Hono offers a lot of possibilities, so give it a try. Also, if you're building a bigger app, [HonoX](https://github.com/honojs/honox), which allows for file-based routing, might be more convenient, so consider using that too.\n\n---\n\n## Author\n\nYusuke Wada \u003chttps://github.com/yusukebe\u003e\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyusukebe%2Furl-shortener","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyusukebe%2Furl-shortener","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyusukebe%2Furl-shortener/lists"}