Créer un raccourcisseur d'URL avec Nitro et Cloudflare Pages

- Read in english

Dans cet article, nous allons développer un raccourcisseur d'URL en utilisant Nitro et le déployer sur Cloudflare Pages.

Le code source est accessible sur url-shortener.

Nitro représente une nouvelle génération de kit d'outils serveur. Il nous permet de construire des serveurs web avec toutes les fonctionnalités nécessaires et de les déployer à notre convenance.

Cloudflare Pages est une plateforme conçue pour construire et héberger des sites web sur le edge. Elle prend en charge des services comme KV pour créer des applications complètes et à état.

Notre projet est un raccourcisseur d'URL simple qui facilite la conversion d'une longue URL en une version raccourcie. Nous allons utiliser le Cloudflare KV pour stocker les URL et le serveur Nitro pour gérer les requêtes.

Nous allons utiliser :

  • unstorage pour rationaliser le processus de développement en abstraire la couche KV, éliminant ainsi le besoin du Cloudflare Wrangler CLI.
  • ohash pour générer un hash à partir de l'URL afin de prévenir les collisions.
  • nanojsx pour construire les pages HTML en utilisant TSX.
  • pico.css pour styliser l'application.

Initialisation du Projet

Tout d'abord, créez un nouveau projet Nitro :

bash
npx giget@latest nitro url-shortener

Ensuite, naviguez jusqu'au projet et installez les dépendances requises :

bash
cd url-shortener
npm install

Démarrez le serveur de développement pour voir la page par défaut de Nitro :

bash
npm run dev

Ouvrez votre navigateur et visitez http://localhost:3000 pour vérifier la fonctionnalité.

Construction du Raccourcisseur d'URL

Dans un premier temps, installez les paquets nécessaires :

bash
npm install ohash nano-jsx

Générer une URL Raccourcie

Créez une route nommée index.get.tsx dans le répertoire server/routes. Cela servira de page d'accueil de notre raccourcisseur d'URL où les utilisateurs peuvent générer une URL raccourcie à partir d'une longue.

tsx
import { h, Helmet, renderSSR } from 'nano-jsx' // le `h` est crucial 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 présentera un formulaire permettant aux utilisateurs de saisir une URL à raccourcir. Lors de la soumission du formulaire, une requête POST est envoyée à la route /create.

Un gestionnaire d'événements paresseux est utilisé pour générer la vue une seule fois, lorsque la requête atteint le serveur. La réponse est ensuite mise en cache en mémoire et réutilisée pour les requêtes futures, réduisant ainsi la charge computationnelle.

La fonction withTemplate sert d'utilitaire que nous devons construire.

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>`
}

Ceci est un simple template de chaîne où nous incorporons le contenu de l'en-tête et du corps générés par nano-jsx.

Stockage de l'URL

Avant de procéder, installez zod pour valider le corps de la requête.

bash
npm install zod

Créez la route /create pour gérer la requête POST et stocker l'URL dans le KV. Cette route est étiquetée create.post.tsx et se trouve dans le répertoire server/routes.

tsx
import { h, Helmet, renderSSR } from 'nano-jsx'
import { hash } from 'ohash'
import { z } from 'zod' // le `h` est essentiel 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 ce segment, nous utilisons la fonction readValidatedBody pour valider le corps de la requête. Cela assure que le champ url est une URL légitime, sinon une erreur est générée.

Nous récupérons l'URL de la requête en utilisant la fonction getRequestURL de h3.

Le hash est créé à partir de l'URL du corps en utilisant la fonction hash de ohash, garantissant une génération de hash cohérente pour éviter les collisions.

L'URL est stockée dans le KV en utilisant la fonction useStorage de unstorage, employant le namespace data pour stocker les URL, préconfiguré pour notre utilisation.

Stockage KV

À la fin de ce gestionnaire d'événements, une route est renvoyée avec l'URL raccourcie que les utilisateurs peuvent copier et utiliser.

Pour le développement, tout fonctionne correctement, 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 le namespace data, comme dans le développement, afin d'utiliser le driver cloudflare-kv-binding et le binding url-shortener, spécifié sous la clé $production. Cela indique que la configuration s'applique uniquement à l'environnement de production pour accéder au Cloudflare KV.

Configuration spécifique à l'environnement

Redirection vers l'URL Originale

Enfin, nous devons établir une route pour gérer les URL raccourcies et rediriger les utilisateurs vers l'URL principale. Cette route est nommée [id].get.ts et est située dans le répertoire 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)
})

Ici, nous obtenons simplement l'id à partir des paramètres du routeur et utilisons useStorage pour récupérer l'URL du KV. Si l'URL n'existe pas, une erreur 404 est générée. Sinon, l'utilisateur est redirigé vers l'URL originale.

Déployer le Raccourcisseur d'URL

Félicitations ! Nous avons réussi à créer un raccourcisseur d'URL de base en utilisant Nitro. La prochaine étape consiste à le déployer sur Cloudflare Pages. Rassurez-vous, c'est simple grâce aux fournisseurs de déploiement sans configuration.

Fournisseurs Zero Config

Warning

Un compte Cloudflare et un dépôt GitHub sont nécessaires pour continuer avec le déploiement. Tout d'abord, connectez-vous au tableau de bord Cloudflare et sélectionnez un compte. Naviguez vers Accueil du compte, choisissez Workers & Pages, et créez une nouvelle application en utilisant le bouton en haut à droite. Sélectionnez l'onglet Pages et suivez les instructions pour établir une connexion GitHub.

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

Maintenant, attendons que le déploiement soit terminé. Une fois terminé, vous pouvez accéder au raccourcisseur d'URL en utilisant l'URL fournie par Cloudflare Pages.

Cependant, il se peut qu'il ne fonctionne pas adéquatement initialement, car un namespace KV n'a pas été lié. Cela nécessite un lien manuel. Allez dans les paramètres du projet, sélectionnez les fonctions et faites défiler jusqu'à ce que vous trouviez liaisons de namespace KV. Ajoutez une liaison avec le nom url-shortener et choisissez le namespace désiré.

Note

Créer un namespace KV est réalisable dans la section Workers & Pages. Redéployez votre application pour tenir compte de la nouvelle liaison de namespace KV. Une fois terminé, le raccourcisseur d'URL sera pleinement fonctionnel. ✨

Note

Si vous n'avez pas l'intention de maintenir le projet, n'oubliez pas de le supprimer du tableau de bord Cloudflare Pages afin d'éviter des dépenses inutiles.

Améliorations et Considérations Futures

Pour améliorer la sécurité, envisagez d'ajouter une simple protection CSRF en utilisant la syntaxe des objets gestionnaires. Modifiez le fichier create.post.tsx pour inclure un gestionnaire avant la requête pour la vérification de l'en-tête d'origine.

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: '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 mettons en place un gestionnaire avant la requête pour vérifier l'en-tête d'origine. S'il y a un désaccord, une erreur est générée ; sinon, la requête passe au gestionnaire principal.

Note

Pour des informations complètes sur la protection CSRF, consultez la Fiche de Prévention contre les Attaques CSRF par OWASP. Valider l'en-tête d'origine est une approche de défense basique pour CSRF. Cependant, il est conseillé de mettre en œuvre un mécanisme de protection plus robuste, tel qu'un token.

Conclusion

Développer avec Nitro et Cloudflare Pages est simple et offre une expérience de développeur exceptionnelle grâce à la possibilité d'utiliser TSX et un stockage de développement, éliminant le besoin du Cloudflare Wrangler CLI.

Profitez de votre parcours de développement avec Nitro et Cloudflare Pages ! 🚀

Note

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

Photo de profil d'Estéban

Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web.

Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.

Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.

J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous—cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !

Soutenir mon travail
Suivez-moi sur