AccueilA proposContact

URL shortener avec Cloudflare Workers

Par Franck Anso
Publié dans Tech
08 sept. 2022
5 min

Nous allons réaliser un raccourcisseur d’URL basique en utilisant les services proposés par Cloudflare. Cela va nous permettre de voir Cloudflare Workers et KV.

Qu’est-ce que Cloudflare Workers ?

image

Cloudflare Workers propose un environnement d’exécution serverless qui fonctionne sur le réseau de Cloudflare basé sur plus de 200 villes au niveau mondial. Sous le capot, Cloudflare Workers utilise le moteur V8 de Google. C’est le même engine que celui de Node.js. 

Qu’est-ce que Cloudflare Workers KV ?

Cloudflare Workers KV est un espace de stockage clé-valeur sans serveur qui fonctionne en conjonction avec Cloudflare Workers. Les bases de données clé-valeur sont très performantes en lecture. Les données sont écrites dans un petit nombre de data centers centralisés et sont ensuite mises en cache après l’accès. Il possède également la possibilité d’automatiquement supprimer la donnée basée sur une clé d’expiration ce qui va nous être bien utile.

Pré-requis

Pour réaliser ce petit projet, vous avez besoin :

  • d’un compte Cloudflare
  • de NodeJS 16 ou 18
  • de Wrangler CLI

À noter que les free tiers de Cloudflare sont très généreux et que vous pourrez faire l’entièreté de cet article sans carte bleue.

Qu’allons-nous réaliser ?

Nous allons réaliser un worker avec deux routes d’API :

  • POST /shorten : Génère un code et sauvegarde dans KV le code (clé) et l’URL (valeur) renseignée.
  • GET /:slug : Va chercher dans KV si une URL existe pour le code en paramètre. Si oui, l’utilisateur est redirigé vers cette URL.

Maintenant que l’on sait où nous allons, nous pouvons nous lancer !

Installation de Wrangler CLI

Wrangler CLI est un utilitaire en ligne de commande pour construire des workers sur Cloudflare Workers. Il va nous permettre d’initialiser un projet, de le lancer en local et de le déployer sur les infrastructures de Cloudflare.

Dans un terminal, installer Wrangler CLI via la commande suivante :

$ npm install -g wrangler

ou avec yarn :

$ yarn global add wrangler

Une fois fait, vous pouvez vérifier la bonne installation en exécutant la commande suivante :

$ wrangler --version

Authentification

Afin de faire le lien entre Wrangler CLI et votre compte Cloudflare, vous allez devoir passer par un mécanisme d’authentification.

Pour cela, saisissez la commande suivante :

$ wrangler login

Une fenêtre de votre navigateur favori va s’ouvrir vous demandant d’autoriser l’accès à votre compte à Wrangler CLI. Une fois que vous avez accepté, vous pouvez refermer la page et retourner sur votre terminal.

Vous pouvez vérifier que l’identification a bien eu lieu via la commande :

$ wrangler whoami

Vous devriez avoir en retour quelque chose de similaire à ceci :

⛅️ wrangler 2.0.28
--------------------
Getting User settings...
👋 You are logged in with an OAuth Token, associated with the email '[email protected]'!
┌───────────────────────────────────┬──────────────────────────────────┐
│ Account Name │ Account ID │
├───────────────────────────────────┼──────────────────────────────────┤
[email protected]'s Account │ <account_id>
└───────────────────────────────────┴──────────────────────────────────┘
🔓 Token Permissions: If scopes are missing, you may need to logout and re-login.
Scope (Access)
- account (read)
- user (read)
- workers (write)
- workers_kv (write)
- workers_routes (write)
- workers_scripts (write)
- workers_tail (read)
- pages (write)
- zone (read)
- offline_access
Retrieving cached values for userId from node_modules/.cache/wrangler

Création du projet

Dans un nouveau dossier, saisissez la commande suivante :

$ wrangler init shorten

Le CLI va vous poser une série de questions, répondez toujours par oui.
À la question “Would you like to create a Worker at src/index.ts?”, répondez “Fetch handler”.

⛅️ wrangler 2.0.28
-------------------------------------------------------
Using npm as package manager.
✨ Created wrangler.toml
Would you like to use git to manage this Worker? (y/n)
✨ Initialized git repository
No package.json found. Would you like to create one? (y/n)
✨ Created package.json
Would you like to use TypeScript? (y/n)
✨ Created tsconfig.json
Would you like to create a Worker at src/index.ts?
None
❯ Fetch handler
Scheduled handler

Vous allez obtenir un projet nommé shorten avec les éléments suivants :

  • un dossier .git signifiant que vous êtes prêt à envoyer votre code vers un repo distant
  • un fichier package.json
  • un fichier tsconfig.json avec toute la configuration TypeScript
  • un fichier src/index.ts contenant un classique “Hello World!“.

Vous pouvez essayer de lancer ce projet via la commande suivante :

$ wrangler dev

Cela va lancer notre Worker sur http://localhost:8787. Si vous appuyez sur “b”, une fenêtre de navigateur va s’ouvrir et vous verrez alors le résultat de notre Worker.

hello world message

Comment cela fonctionne-t-il ?

Si vous ouvrez le fichier src/index.ts, vous devriez trouver ceci :

/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `wrangler dev src/index.ts` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `wrangler publish src/index.ts --name my-worker` to publish your worker
*
* Learn more at https://developers.cloudflare.com/workers/
*/
export interface Env {
// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
// MY_KV_NAMESPACE: KVNamespace;
//
// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
// MY_DURABLE_OBJECT: DurableObjectNamespace;
//
// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
// MY_BUCKET: R2Bucket;
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
return new Response("Hello World!");
},
};

Le code ci-dessus défini tout d’abord notre environnement Env avec beaucoup de commentaires sur l’interface. Cela permet de faire le lien entre les différents systèmes que propose Cloudflare et notamment les datastores. Nous y reviendrons un peu plus loin.

Ensuite, nous voyons que notre index.ts exporte une fonction fetch. Nous retrouvons des similitudes avec les Web Workers. D’ailleurs le nom Cloudflare Workers n’a certainement pas été choisi au hasard.

Dans l’exemple, la fonction fetch renvoie une réponse contenant “Hello World!“. C’est cette réponse que nous récupérons dans le navigateur.

Mise en place des datastore

Maintenant que nous avons un début de projet, nous allons pouvoir commencer à construire notre solution. Pour cela, nous allons avoir besoin d’un datastore pour stocker nos codes/url.

Dans un terminal, positionnez-vous dans le dossier du projet créé précédemment et saisissez la commande suivante :

$ wrangler kv:namespace create KV

Nous allons refaire la même opération en ajoutant --preview, pour avoir un datastore pour nos développements :

$ wrangler kv:namespace create KV --preview

À chacune des créations, un identifiant va vous être fourni. Veuillez les garder de côté. Nous allons en avoir besoin par la suite.

Ouvrez maintenant le fichier wrangler.toml qui se situe à la racine du projet. Vous devriez avoir quelque chose de similaire à ceci :

name = "shorten"
main = "src/index.ts"
compatibility_date = "2022-09-06"

Nous allons ajouter nos datastore à ce fichier :

name = "shorten"
main = "src/index.ts"
compatibility_date = "2022-09-06"
kv_namespaces = [
{ binding = "KV", id = "<prod_id>", preview_id = "<preview_id>" }
]

Pensez à remplacer <prod_id> et <preview_id> par les id que vous aviez mis de côté à l’étape précédente.

Retournez maintenant dans src/index.ts et finissez le binding de votre datastore dans l’interface Env :

export interface Env {
// Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
KV: KVNamespace;
//
// Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
// MY_DURABLE_OBJECT: DurableObjectNamespace;
//
// Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
// MY_BUCKET: R2Bucket;
}

Le nom de la propriété dans Env doit être identique à ce que vous avez mis dans le fichier wrangler.toml dans la propriété binding. Ici, nous avons mis KV.

Mise en place du routing

Pour rappel, nous allons devoir gérer deux routes dans notre projet :

  • Une pour la création d’une URL raccourcie ;
  • La deuxième pour la redirection.

Pour cela, nous allons utiliser le package itty-router. C’est un petit routeur sans aucune dépendance qui gère les paramètres d’URL et le parsing de query construit spécialement pour faire des Workers sur Cloudflare. Nous allons en profiter pour ajouter nanoid pour générer nos codes.

Dans un terminal, positionnez-vous dans le projet et saisissez la commande suivante :

$ npm install itty-router itty-router-extras nanoid

Maintenant que nous avons nos dépendances, créez un fichier src/router.ts avec le contenu ci-dessous :

import {Router} from "itty-router";
import {missing} from "itty-router-extras";
const router = Router();
router
.all('*', () => missing('Not found.'));
export default router;

Nous commençons par créer notre router via const router = Router();. Puis, nous pluggons par défaut toutes les routes à une erreur 404. Enfin, nous exportons ce router.

Dans le fichier src/index.ts, modifier le contenu comme ci-dessous pour plugger la réponse à notre router :

export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
return router.handle(request, env);
},
};

À ce niveau, notre worker ne fait plus grand-chose à part renvoyer des erreurs 404…

Création de la route POST /shorten

Nous allons maintenant nous occuper de faire le nécessaire pour enregistrer les URLs envoyées par les utilisateurs en leur associant un code dans le datastore.

Dans un dossier src/handlers, créez un fichier shorten.ts avec le code ci-dessous :

import {Request} from "itty-router";
import {error} from "itty-router-extras";
import {nanoid} from "nanoid";
import {Env} from "../index";
const shortenHandler = async (request: Request, env: Env) => {
const requestBody = await request.json?.();
if (!requestBody?.url) {
return error(400, 'Missing `url` field.');
}
const slug = nanoid();
await env.KV.put(slug, requestBody.url, {expirationTtl: 3600});
return new Response(JSON.stringify({
"url": `${new URL(request.url).origin}/${slug}`
}), {
headers: {
'Access-Control-Allow-Origin': '*',
'Content-type': 'application/json'
},
status: 200
});
}
export default shortenHandler;

Nous récupérons l’URL à enregistrer via requestBody.url. Si celle-ci est manquante, nous renvoyons une erreur 400.

Nous générons ensuite un code via nanoid grâce à const slug = nanoid(); puis nous l’enregistrons sur Cloudflare KV via await env.KV.put(slug, requestBody.url, {expirationTtl: 3600});.

Le mot clé awaitpermet d’attendre d’avoir la confirmation d’écriture. Dans le cas contraire, nous pourrions être sorti du worker sans avoir correctement écrit dans le datastore.

Nous mettons un expirationTtl d’une heure pour que les entrées soient automatiquement supprimées par le datastore sans que nous ayons à gérer la logique de notre côté.

Pour finir, nous créons la réponse à renvoyer au client avec l’URL raccourcie.

Ouvrez maintenant le fichier src/router.tset ajoutez notre nouvelle route comme ci-dessous :

import {Router} from "itty-router";
import {missing} from "itty-router-extras";
import shortenHandler from "./handlers/shorten";
router
.post('/shorten', shortenHandler)
.all('*', () => missing('Not found.'));
export default router;

Vous pouvez tester en lançant le projet via wrangler dev puis en envoyant un curl comme ceci :

curl -X "POST" "http://127.0.0.1:8787/shorten" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}'

Vous devriez avoir en réponse l’URL raccourcie. Vous pouvez également aller sur Cloudflare KV pour voir l’entrée dans le datastore preview.

Création de la route GET /:slug

Il ne reste plus qu’à créer une route pour pouvoir rediriger l’utilisateur en fonction du code reçue.

Dans le dossier src/handlers, créez un fichier redirect.ts avec le code ci-dessous :

import {Request} from "itty-router";
import {error, missing} from "itty-router-extras";
import {Env} from "../index";
const redirectHandler = async (request: Request, env: Env) => {
const {params} = request
if (!params?.slug) {
return error(400, 'Missing `slug` param.');
}
let link = await env.KV.get(params.slug);
if (!link) {
return missing(`Slug '${params.slug}' not found.`);
}
return new Response(null, {
headers: {Location: link},
status: 301
});
}
export default redirectHandler;

Nous commençons par récupérer le code dans les paramètres via params.slug puis nous interrogeons le datastore via await env.KV.get(params.slug);. Si nous trouvons une URL nous effectuons la redirection sinon nous renvoyons une erreur 404.

Comme précédemment, vous devez ajouter cette route au fichier src/router.ts :

import {Router} from "itty-router";
import {missing} from "itty-router-extras";
import shortenHandler from "./handlers/shorten";
import redirectHandler from "./handlers/redirect";
const router = Router();
router
.post('/shorten', shortenHandler)
.get('/:slug', redirectHandler)
.all('*', () => missing('Not found.'));
export default router;

Maintenant, vous pouvez démarrez le projet via wrangler dev. Ouvrez une fenêtre du navigateur et saisissez l’URL http://127.0.0.1/<code> pour être redirigé.

Déploiement sur Cloudflare

Nous pouvons facilement déployer notre worker sur Cloudflare via la commande wrangler publish. Le CLI va tout prendre en charge et nous obtiendrons une URL d’accès à notre worker accessible pour tout le monde.

Le code source de cet article est disponible sur Github.

Pour aller plus loin

Vous pouvez maintenant réaliser une interface pour votre raccourcisseur d’URL. De mon côté, j’en ai réalisé une en React disponible sur ce repository Github.

À noter que vous allez être confronté à un problème de CORS. En effet, l’URL de votre interface sera différente de celle de votre worker.

Pour le résoudre, vous devez ajouter une route à votre router comme ceci :

router
.options('/shorten', () => {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept"
}
});
})
.post('/shorten', shortenHandler)
.get('/:slug', redirectHandler)
.all('*', () => missing('Not found.'));
export default router;

Comment je peux vous aider ?

Je suis convaincu que l’avenir est à l’abstraction de la couche serveur pour les équipes de développement que ce soit au niveau du back mais également des bases de données.

Si vous avez un projet serverless, n’hésitez pas à me contacter pour que nous y réfléchissions ensemble.


Tags

#serverless
Previous Article
Qu'est-ce que l'expérience développeur (DevEx - DX) ?

Catégories

Management
Tech
UI/UX

Publications liées

Création d'un formulaire de contact avec Twilio Serverless Function et Sendgrid

15 juil. 2022
4 min

Liens rapides

Social Media