---
contentId: 5b969fcd-0a41-4bc1-ae7e-3aca6d1f48a5
title: Build a URL Shortener with Nitro on Cloudflare Pages
description: Learn how to create a URL shortener using Nitro and deploy it on Cloudflare Pages, enabling effortless edge app development.
date: 2024-03-19
---

In this article, we will develop a **URL shortener** utilizing [**Nitro**](https://nitro.unjs.io) and deploy it on [**Cloudflare Pages**](https://pages.cloudflare.com/).

The source code is accessible on <GitHubLink repo="barbapapazes/url-shortener" />.

**Nitro** represents a new generation of **server toolkit**. It empowers us to construct web servers with all necessary functionalities and deploy them at our convenience.

**Cloudflare Pages** is a platform designed to build and host websites on the edge. It supports services like [**KV**](https://developers.cloudflare.com/kv/) to create comprehensive full-stack stateful applications.

Our project is a straightforward **URL shortener** that facilitates converting a long URL into a shortened version. We will leverage the **Cloudflare KV** to store the URLs and the **Nitro server** to manage requests.

We will utilize:

- [**unstorage**](https://unstorage.unjs.io) to streamline the development process by abstracting the **KV** layer, eliminating the need for the **Cloudflare Wrangler CLI**.
- [**ohash**](https://ohash.unjs.io) to generate a **hash** from the URL to prevent collisions.
- [**nanojsx**](https://nanojsx.io/) to build the **HTML pages** using **TSX**.
- [**pico.css**](https://picocss.com) for styling the application.

## Project initialization

First, create a new **Nitro project**:

```bash
npx giget@latest nitro url-shortener
```

Subsequently, navigate to the project and install the required **dependencies**:

```bash
cd url-shortener
npm install
```

Start the **development server** to view the default Nitro page:

```bash
npm run dev
```

Open your browser and visit [http://localhost:3000](http://localhost:3000) to verify functionality.

## Constructing the URL shortener

Initially, install the necessary packages:

```bash
npm install ohash nano-jsx
```

### Generate a short URL

Create a **route** named `index.get.tsx` within the `server/routes` directory. This will serve as the **home page** of our URL shortener where users can generate a shortened URL from a long one.

```tsx [server/routes/index.get.tsx]
import { h, Helmet, renderSSR } from 'nano-jsx' // the `h` is critical here
import { withTemplate } from '../resources/template'

export default defineLazyEventHandler(() => {
  const App = () => {
    return (
      <div>
        <Helmet>
          <title>URL Shortener with Nitro</title>
        </Helmet>
        <h2>Shorten a URL</h2>
        <form action="/create" method="POST">
          <input type="url" name="url" placeholder="URL to shorten" autocomplete="off" />
          <button type="submit">Create</button>
        </form>
      </div>
    )
  }
  const app = renderSSR(<App />)
  const { body, head } = Helmet.SSR(app)

  const page = withTemplate({
    body,
    head,
  })

  return defineEventHandler(() => {
    return page
  })
})
```

This route will present a form enabling users to input a URL to shorten. Upon form submission, a **POST request** is sent to the `/create` route.

A **lazy event handler** is utilized to generate the view only once, when a request reaches the server. The response is then **cached in-memory** and reused for future requests, reducing the computational load.

The `withTemplate` function serves as a utility that we must construct.

```ts [server/resources/template.tsx]
interface LayoutProps {
  body: string
  head: string[]
}

export function withTemplate(props: LayoutProps) {
  const { head, body } = props

  return /* html */`<html>
      <head>
       <link
          rel="stylesheet"
          href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
        />
        ${head.join('\n')}
      </head>
      <body>
        <header class="container">
          <h1>
            <a href="/">URL Shortener with Nitro</a>
          </h1>
        </header>
        <main class="container">
          ${body}
        </main>
      </body>
    </html>`
}
```

This is a simple **string template** where we incorporate the head and body content generated by **nano-jsx**.

### URL storage

Before proceeding, install `zod` to validate the request body.

```bash
npm install zod
```

Create the `/create` route to manage the **POST request** and store the URL in the **KV**. This route is labeled `create.post.tsx` and resides in the `server/routes` directory.

```tsx [server/routes/create.post.tsx]
import { h, Helmet, renderSSR } from 'nano-jsx'
import { hash } from 'ohash'
import { z } from 'zod' // the `h` is essential here
import { withTemplate } from '../resources/template'

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, z.object({
    url: z.string().url(),
  }).parse)

  const requestURL = getRequestURL(event)
  const id = hash(body.url)
  const shortenURL = new URL(`/${id}`, requestURL).href

  await useStorage('data').setItem(id, body.url)

  const App = () => {
    return (
      <div>
        <Helmet>
          <title>Created</title>
        </Helmet>
        <h2>Created and Ready</h2>
        <input
          type="text"
          value={shortenURL}
          autofocus
        />
      </div>
    )
  }

  const app = renderSSR(<App />)
  const { body: nanoBody, head } = Helmet.SSR(app)

  return withTemplate({
    body: nanoBody,
    head,
  })
})
```

In this segment, we utilize the `readValidatedBody` function to **validate the request body**. This assures that the `url` field is a legitimate URL, throwing an error otherwise.

We retrieve the **request URL** using the `getRequestURL` function from `h3`.

The **hash** is created from the body URL using the `hash` function from **ohash**, ensuring consistent hash generation to avoid collisions.

The URL is stored in the **KV** using the `useStorage` function from **unstorage**, employing the `data` namespace to store the URLs, pre-configured for our use.

<CalloutReadMore text="KV Storage" href="https://nitro.unjs.io/guide/storage#usage" />

At the end of this event handler, a route is returned with the shortened URL that users can copy and utilize.

For development, everything functions correctly, but we must update this configuration, `nitro.config.ts`, for the production environment.

```ts [nitro.config.ts]
export default defineNitroConfig({
  srcDir: 'server',
  $production: {
    storage: { data: { driver: 'cloudflare-kv-binding', binding: 'url-shortener' } },
  },
})
```

In this configuration, we define the `data` namespace, as in development, to use the **cloudflare-kv-binding** driver and the `url-shortener` binding, specified under the key `$production`. This indicates that the configuration applies solely to the **production environment** for accessing the **Cloudflare KV**.

<CalloutReadMore text="Environment-specific configuration" href="https://github.com/unjs/c12#environment-specific-configuration" />

### Redirecting to the original URL

Finally, we need to establish a route to handle shortened URLs and redirect users to the primary URL. This route is named `[id].get.ts` and is situated in the `server/routes` directory.

```ts [server/routes/[id].get.ts]
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'short')

  const value = await useStorage<string>('data').getItem(id)

  if (!value) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Not Found',
    })
  }

  return sendRedirect(event, value)
})
```

Here, we simply obtain the `id` from the router parameters and use `useStorage` to fetch the **URL** from the **KV**. If the URL does not exist, a **404 error** is thrown. Otherwise, the user is redirected to the original URL.

## Deploying the URL shortener

Congratulations! We have successfully created a basic URL shortener using Nitro. The next step is to **deploy** it on Cloudflare Pages. Rest assured, it's straightforward thanks to the **zero-config deployment providers**.

<CalloutReadMore text="Zero Config Providers" href="https://nitro.unjs.io/deploy#zero-config-providers" />

> [!WARNING]
> A Cloudflare account and a GitHub repository are required to continue with the deployment.
Initially, log in to the Cloudflare dashboard and select an account. Navigate to Account Home, choose Workers & Pages, and create a new application using the button at the top right. Select the Pages tab and follow the instructions to establish a GitHub connection.

Select the repository, add the build command `npm run build`, specify the output directory `dist`, include an environment variable `NODE_ENV` with the value `production`, and save and deploy.

Now, let's wait for the deployment to complete. Once done, you can access the URL shortener using the URL provided by **Cloudflare Pages**.

**However**, it may not function adequately initially, as a **KV namespace** has not been bound. This requires manual binding. Go to the project settings, select functions, and scroll until you find **KV namespace bindings**. Add a binding with the name `url-shortener` and choose the desired namespace.

> [!NOTE]
> Creating a **KV namespace** is achievable in the **Workers & Pages** section.
Re-deploy your application to account for the new KV namespace binding. Once completed, the URL shortener will be fully functional. :sparkles:

> [!NOTE]
> If you do not intend to maintain the project, remember to delete it from the Cloudflare Pages dashboard to avoid unnecessary expenses.

## Enhancements and future considerations

To enhance security, consider adding simple **CSRF protection** using the handler object syntax. Modify the `create.post.tsx` file to include a pre-request handler for origin header verification.

```tsx [server/routes/create.post.tsx]
import { h, Helmet, renderSSR } from 'nano-jsx'
import { hash } from 'ohash'
import { z } from 'zod'
import { withTemplate } from '../resources/template'

export default defineEventHandler({
  onBeforeResponse: async (event) => {
    const requestURL = getRequestURL(event).origin
    const origin = getRequestHeader(event, 'origin')

    if (!origin) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Bad Request',
      })
    }

    if (origin !== requestURL) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Forbidden',
      })
    }
  },
  handler: async (event) => {
    const body = await readValidatedBody(event, z.object({
      url: z.string().url(),
    }).parse)

    const requestURL = getRequestURL(event)
    const id = hash(body.url)
    const shortenURL = new URL(`/${id}`, requestURL).href

    await useStorage('data').setItem(id, body.url)

    const App = () => {
      return (
        <div>
          <Helmet>
            <title>Created</title>
          </Helmet>
          <h2>Created and Ready</h2>
          <input
            type="text"
            value={shortenURL}
            autofocus
          />
        </div>
      )
    }

    const app = renderSSR(<App />)
    const { body: nanoBody, head } = Helmet.SSR(app)

    return withTemplate({
      body: nanoBody,
      head,
    })
  },
})
```

We set up a pre-request handler to check the **origin** header. If there is a mismatch, an error is raised; otherwise, the request proceeds to the main handler.

> [!NOTE]
> For comprehensive information on **CSRF protection**, refer to the [Cross-Site Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) by OWASP. Validating the **origin** header is a basic defense approach for **CSRF**. However, it is advisable to implement a more robust protection mechanism such as a token.

## Conclusion

Developing with **Nitro** and **Cloudflare Pages** is straightforward and provides an exceptional developer experience due to the ability to utilize **TSX** and development storage, eliminating the need for the **Cloudflare Wrangler CLI**.

Enjoy your development journey with **Nitro** and **Cloudflare Pages**! 🚀

> [!NOTE]
> The URL shortener is inspired by [Yusuke Wada's URL Shortener](https://github.com/yusukebe/url-shortener).
