Raccourcisseur d'URL avec Nitro sur Cloudflare Pages

- Read in English

Dans cet article, nous allons créer notre propre raccourcisseur d'URL en utilisant Nitro et le déployer sur Cloudflare Pages.

Le code source est disponible sur url-shortener.

Nitro est la prochaine génération d'outils serveur. Il nous permet de créer des serveurs web avec tout ce dont nous avons besoin et de les déployer là où nous le souhaitons.

Cloudflare Pages est une plate-forme pour créer et héberger des sites web à la périphérie. Il est possible d'utiliser des services comme KV pour créer des applications complètes et avec état.

Notre projet est un simple raccourcisseur d'URL qui nous permettra de créer une courte URL à partir d'une longue. Nous utiliserons le Cloudflare KV pour stocker les URL et le serveur Nitro pour gérer les requêtes.

Nous allons utiliser :

  • unstorage pour abstraire la couche KV et simplifier le développement, car nous n'avons pas besoin d'utiliser l'interface en ligne de commande Cloudflare Wrangler.
  • ohash pour créer un hachage à partir de l'URL et éviter les collisions.
  • nanojsx pour créer les pages HTML en utilisant TSX.
  • pico.css pour styliser l'application.

Initialiser le Projet

Tout d'abord, nous devons créer un nouveau projet Nitro :

bash
npx giget@latest nitro url-shortener

Ensuite, nous pouvons entrer dans le projet et installer les dépendances requises :

bash
cd url-shortener
npm install

Nous pouvons démarrer le serveur de développement pour voir la page par défaut de Nitro :

bash
npm run dev

Ouvrez votre navigateur et accédez à http://localhost:3000 pour vérifier que tout fonctionne.

Créer le Raccourcisseur d'URL

Avant toute chose, nous devons installer les packages nécessaires :

bash
npm install ohash nano-jsx

Créer une URL Courte

Tout d'abord, nous devons créer une route appelée index.get.tsx dans le dossier server/routes. Cette route sera la page d'accueil de notre raccourcisseur d'URL où l'utilisateur pourra créer une courte URL à partir d'une longue.

tsx
import { Helmet, h, renderSSR } from 'nano-jsx' // le `h` est très important ici
import { withTemplate } from '../resources/template'

export default defineLazyEventHandler(() => {
  const App = () => {
    return (
      <div>
        <Helmet>
          <title>Raccourcisseur d'URL avec Nitro</title>
        </Helmet>
        <h2>Raccourcir une URL</h2>
        <form action="/create" method="POST">
          <input type="url" name="url" placeholder="URL à raccourcir" autocomplete="off" />
          <button type="submit">Créer</button>
        </form>
      </div>
    )
  }
  const app = renderSSR(<App />)
  const { body, head } = Helmet.SSR(app)

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

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

Cette route affichera un formulaire où l'utilisateur peut entrer une URL à raccourcir. Lorsque le formulaire est soumis, il enverra une requête POST à la route /create.

Nous utilisons un gestionnaire d'événements paresseux pour créer la vue une seule fois, lorsqu'une requête atteint le serveur. Ensuite, la réponse est mise en cache en mémoire et réutilisée pour les requêtes suivantes. Cela est utile pour éviter de créer la vue à chaque requête, car elle est identique pour tous.

La fonction withTemplate est un utilitaire que nous devons créer.

ts
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="/">Raccourcisseur d'URL avec Nitro</a>
          </h1>
        </header>
        <main class="container">
          ${body}
        </main>
      </body>
    </html>`
}

C'est un simple modèle de chaîne où nous injectons le contenu de l'en-tête et du corps créé par nano-jsx.

Stocker l'URL

Avant de passer à cette partie, nous devons installer zod pour valider le corps de la requête.

bash
npm install zod

Maintenant, nous devons créer la route /create pour gérer la requête POST et stocker l'URL dans le KV. La route s'appelle create.post.tsx et se trouve dans le dossier server/routes.

tsx
import { z } from 'zod'
import { hash } from 'ohash'
import { Helmet, h, renderSSR } from 'nano-jsx' // le `h` est très important ici
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>Créé</title>
        </Helmet>
        <h2>Créé et prêt</h2>
        <input
          type="text"
          value={shortenURL}
          autofocus
        />
      </div>
    )
  }

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

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

Dans cette route, nous utilisons la fonction readValidatedBody pour valider le corps de la requête. Cela nous assure que le champ url est une URL valide. Sinon, une erreur sera levée.

Ensuite, nous obtenons l'URL de la requête à l'aide de la fonction getRequestURL de h3.

Nous créons un hachage à partir de l'URL du corps en utilisant la fonction hash de ohash. Cela est utile pour garantir qu'une URL aura toujours le même hachage et pour éviter les collisions.

Nous stockons l'URL dans le KV en utilisant la fonction useStorage de unstorage. Nous utilisons l'espace de noms data pour stocker les URL qui est préconfiguré pour nous.

Stockage KV

À la fin de ce gestionnaire d'événements, nous retournons une route avec l'URL raccourcie que l'utilisateur peut copier et utiliser.

En développement, tout est en ordre, mais nous devons mettre à jour cette configuration, nitro.config.ts, pour l'environnement de production.

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

Dans cette configuration, nous définissons l'espace de noms data, tout comme en développement, pour utiliser le driver cloudflare-kv-binding et le binding url-shortener sous la clé $production. Cela signifie que cette configuration ne sera utilisée que dans l'environnement de production pour accéder à Cloudflare KV.

Configuration spécifique à l'environnement

Rediriger vers l'URL

Enfin, nous devons créer une route pour gérer les URL raccourcies et rediriger l'utilisateur vers l'URL originale. Cette route s'appelle [id].get.ts et se trouve dans le dossier server/routes.

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: 'Non trouvé',
    })
  }

  return sendRedirect(event, value)
})

Rien de vraiment spécial ici. Nous obtenons l'id des paramètres du routeur et nous utilisons la fonction useStorage pour obtenir l'URL du KV. Si l'URL n'existe pas, nous levons une erreur 404. Sinon, nous redirigeons l'utilisateur vers l'URL originale.

Déployer le Raccourcisseur d'URL

C'est tout ! Nous avons créé un simple raccourcisseur d'URL avec Nitro. Maintenant, nous devons le déployer sur Cloudflare Pages. Pas de panique, c'est facile grâce à des fournisseurs de déploiement sans configuration.

Fournisseurs sans configuration

Warning

Nous devons avoir un compte Cloudflare et un dépôt GitHub pour déployer l'application pour continuer. Tout d'abord, connectez-vous au tableau de bord Cloudflare et sélectionnez un compte. Ensuite, cliquez sur Accueil du compte, sélectionnez Workers & Pages. Créez une nouvelle application en utilisant le bouton en haut à droite, puis l'onglet Pages, et suivez le processus en utilisant une connexion Git.

Sélectionnez le dépôt, ajoutez la commande de construction npm run build, le répertoire de sortie dist, ajoutez une variable d'environnement NODE_ENV avec la valeur production et enregistrez et déployez.

Maintenant, nous n'avons plus qu'à attendre la fin du déploiement. Une fois terminé, nous pouvons accéder au raccourcisseur d'URL en utilisant l'URL fournie par Cloudflare Pages.

Mais, cela ne fonctionnera pas vraiment puisque nous n'avons pas lié un espace de noms KV. Nous devons le faire manuellement. Allez dans le projet, paramètres, fonctions et faites défiler jusqu'à ce que vous voyiez les liaisons d'espace de noms KV. Ensuite, ajoutez une liaison avec le nom url-shortener et l'espace de noms que vous souhaitez utiliser.

Note

Il est possible de créer un espace de noms KV dans la section Workers & Pages. Maintenant, nous devons redéployer notre application pour prendre en compte la nouvelle liaison d'espace de noms KV. Une fois cela fait, nous pouvons utiliser le raccourcisseur d'URL. ✨

Note

Si vous ne prévoyez pas d'utiliser le projet, n'oubliez pas de le supprimer du tableau de bord Cloudflare Pages pour éviter des coûts inutiles.

Aller Plus Loin

Nous pouvons ajouter une simple protection CSRF en utilisant la syntaxe de l'objet gestionnaire. Nous mettons à jour le fichier create.post.tsx pour ajouter un gestionnaire avant la requête afin de vérifier l'en-tête origin.

tsx
import { z } from 'zod'
import { hash } from 'ohash'
import { Helmet, h, renderSSR } from 'nano-jsx'
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: 'Mauvaise requête',
      })
    }

    if (origin !== requestURL) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Interdit',
      })
    }
  },
  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>Créé</title>
          </Helmet>
          <h2>Créé et prêt</h2>
          <input
            type="text"
            value={shortenURL}
            autofocus
          />
        </div>
      )
    }

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

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

Nous définissons un gestionnaire avant la requête pour vérifier l'en-tête origin. S'il y a un désaccord, nous levons une erreur. Sinon, nous continuons vers le gestionnaire principal.

Note

Veuillez lire Cross-Site Request Forgery Prevention Cheat Sheet d'OWASP pour comprendre la protection CSRF. Vérifier l'en-tête origin est un moyen simple de se protéger contre la CSRF mais considéré comme une défense en profondeur. Il est recommandé d'utiliser une protection plus avancée comme un token.

Enfin

Construire avec Nitro et Cloudflare Pages est assez facile et offre une excellente expérience développeur grâce à la possibilité d'utiliser TSX et à un stockage de développement évitant l'utilisation de l'interface en ligne de commande Cloudflare Wrangler.

Amusez-vous à construire avec Nitro et Cloudflare Pages ! 🚀

Note

Le raccourcisseur d'URL est inspiré par le raccourcisseur d'URL de Yusuke Wada.

Retour aux articles
Soutenez mon travail
Suivez-moi sur