Build a URL Shortener with Nitro on Cloudflare Pages
In this article, we will develop a URL shortener utilizing Nitro and deploy it on Cloudflare Pages.
The source code is accessible on 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 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 to streamline the development process by abstracting the KV layer, eliminating the need for the Cloudflare Wrangler CLI.
- ohash to generate a hash from the URL to prevent collisions.
- nanojsx to build the HTML pages using TSX.
- pico.css for styling the application.
Project Initialization
First, create a new Nitro project:
npx giget@latest nitro url-shortener
Subsequently, navigate to the project and install the required dependencies:
cd url-shortener
npm install
Start the development server to view the default Nitro page:
npm run dev
Open your browser and visit http://localhost:3000 to verify functionality.
Constructing the URL Shortener
Initially, install the necessary packages:
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.
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.
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.
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.
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.
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.
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.
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.
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.
Zero Config ProvidersWarning
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. ✨
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.
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 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.
Thanks for reading! My name is Estéban, and I love to write about web development.
I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.
If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.
I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!