Retour au blog

Architecture Serverless avec JavaScript : Guide Complet avec AWS Lambda et Vercel

Salut HaWkers, serverless a cessé d'être un buzzword pour devenir l'une des architectures les plus adoptées en production. En 2025, comprendre le serverless n'est plus optionnel - c'est une compétence essentielle pour les développeurs qui veulent construire des applications scalables sans gérer d'infrastructure.

Avez-vous déjà pensé à ne plus vous soucier des serveurs, du provisionnement ou de la scalabilité ? Payer uniquement pour ce que vous utilisez ? C'est la promesse du serverless, et dans cet article nous allons explorer comment l'implémenter en pratique.

Qu'est-ce que le Serverless et Pourquoi C'est Important

Serverless ne signifie pas "sans serveur" - cela signifie que vous n'avez pas besoin de gérer les serveurs. L'infrastructure est totalement abstraite, vous permettant de vous concentrer uniquement sur le code.

Serverless Architecture

Avantages du Modèle Serverless

Coût :

  • Payez uniquement pour le temps d'exécution
  • Pas de coûts de serveurs inactifs
  • Scaling automatique sans provisionner de ressources supplémentaires

Opérationnel :

  • Zéro gestion d'infrastructure
  • Deploy simplifié
  • Haute disponibilité automatique

Développement :

  • Focus total sur le code
  • Itération rapide
  • Time-to-market réduit

AWS Lambda : Le Standard de l'Industrie

AWS Lambda est le service serverless le plus utilisé au monde. Voyons comment créer des fonctions robustes avec Node.js.

Structure Basique d'une Lambda

// handler.ts
import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';

interface UtilisateurInput {
  nom: string;
  email: string;
  age?: number;
}

interface ReponseStandard {
  succes: boolean;
  message: string;
  donnees?: unknown;
}

// Fonction utilitaire pour les réponses standardisées
const creerReponse = (
  statusCode: number,
  body: ReponseStandard
): APIGatewayProxyResult => ({
  statusCode,
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Credentials': true
  },
  body: JSON.stringify(body)
});

export const creerUtilisateur: APIGatewayProxyHandler = async (event) => {
  try {
    // Valider le corps de la requête
    if (!event.body) {
      return creerReponse(400, {
        succes: false,
        message: 'Le corps de la requête est obligatoire'
      });
    }

    const donnees: UtilisateurInput = JSON.parse(event.body);

    // Validations métier
    if (!donnees.nom || !donnees.email) {
      return creerReponse(400, {
        succes: false,
        message: 'Le nom et l\'email sont obligatoires'
      });
    }

    if (!donnees.email.includes('@')) {
      return creerReponse(400, {
        succes: false,
        message: 'Email invalide'
      });
    }

    // Simuler la création en base (en production, utilisez DynamoDB ou autre)
    const nouvelUtilisateur = {
      id: Date.now().toString(),
      ...donnees,
      creeA: new Date().toISOString()
    };

    // Log pour CloudWatch
    console.log('Utilisateur créé :', JSON.stringify(nouvelUtilisateur));

    return creerReponse(201, {
      succes: true,
      message: 'Utilisateur créé avec succès',
      donnees: nouvelUtilisateur
    });

  } catch (error) {
    console.error('Erreur lors de la création de l\'utilisateur :', error);

    return creerReponse(500, {
      succes: false,
      message: 'Erreur interne du serveur'
    });
  }
};

Configuration avec Serverless Framework

# serverless.yml
service: api-utilisateurs

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs20.x
  region: eu-west-1
  stage: ${opt:stage, 'dev'}
  memorySize: 256
  timeout: 10
  environment:
    STAGE: ${self:provider.stage}
    TABLE_NAME: ${self:service}-${self:provider.stage}-utilisateurs

  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
            - dynamodb:Query
            - dynamodb:Scan
          Resource:
            - !GetAtt UtilisateursTable.Arn

functions:
  creerUtilisateur:
    handler: src/handlers/utilisateurs.creerUtilisateur
    events:
      - http:
          path: utilisateurs
          method: post
          cors: true

  chercherUtilisateur:
    handler: src/handlers/utilisateurs.chercherUtilisateur
    events:
      - http:
          path: utilisateurs/{id}
          method: get
          cors: true

  listerUtilisateurs:
    handler: src/handlers/utilisateurs.listerUtilisateurs
    events:
      - http:
          path: utilisateurs
          method: get
          cors: true

resources:
  Resources:
    UtilisateursTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.TABLE_NAME}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST

plugins:
  - serverless-esbuild
  - serverless-offline

custom:
  esbuild:
    bundle: true
    minify: true
    sourcemap: true
    target: node20

Intégration avec DynamoDB

// src/lib/dynamodb.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  QueryCommand,
  ScanCommand
} from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({
  region: process.env.AWS_REGION || 'eu-west-1'
});

export const dynamoDB = DynamoDBDocumentClient.from(client);

// src/handlers/utilisateurs.ts
import { APIGatewayProxyHandler } from 'aws-lambda';
import { dynamoDB } from '../lib/dynamodb';
import { GetCommand, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
import { v4 as uuidv4 } from 'uuid';

const TABLE_NAME = process.env.TABLE_NAME!;

export const creerUtilisateur: APIGatewayProxyHandler = async (event) => {
  try {
    const donnees = JSON.parse(event.body || '{}');

    const utilisateur = {
      id: uuidv4(),
      nom: donnees.nom,
      email: donnees.email,
      creeA: new Date().toISOString(),
      miseAJourA: new Date().toISOString()
    };

    await dynamoDB.send(new PutCommand({
      TableName: TABLE_NAME,
      Item: utilisateur,
      ConditionExpression: 'attribute_not_exists(id)'
    }));

    return {
      statusCode: 201,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ succes: true, donnees: utilisateur })
    };
  } catch (error) {
    console.error('Erreur :', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ succes: false, erreur: 'Erreur interne' })
    };
  }
};

export const chercherUtilisateur: APIGatewayProxyHandler = async (event) => {
  try {
    const { id } = event.pathParameters || {};

    if (!id) {
      return {
        statusCode: 400,
        body: JSON.stringify({ succes: false, message: 'ID obligatoire' })
      };
    }

    const resultat = await dynamoDB.send(new GetCommand({
      TableName: TABLE_NAME,
      Key: { id }
    }));

    if (!resultat.Item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ succes: false, message: 'Utilisateur non trouvé' })
      };
    }

    return {
      statusCode: 200,
      body: JSON.stringify({ succes: true, donnees: resultat.Item })
    };
  } catch (error) {
    console.error('Erreur :', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ succes: false, erreur: 'Erreur interne' })
    };
  }
};

export const listerUtilisateurs: APIGatewayProxyHandler = async () => {
  try {
    const resultat = await dynamoDB.send(new ScanCommand({
      TableName: TABLE_NAME,
      Limit: 100
    }));

    return {
      statusCode: 200,
      body: JSON.stringify({
        succes: true,
        donnees: resultat.Items || [],
        total: resultat.Count
      })
    };
  } catch (error) {
    console.error('Erreur :', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ succes: false, erreur: 'Erreur interne' })
    };
  }
};

Vercel Edge Functions : Performance à l'Edge

Vercel offre une expérience serverless focalisée sur la performance, exécutant des fonctions à l'edge (plus proche de l'utilisateur).

API Routes avec Next.js

// app/api/produits/route.ts
import { NextRequest, NextResponse } from 'next/server';

interface Produit {
  id: string;
  nom: string;
  prix: number;
  stock: number;
}

// Simulation de base de données
const produits: Produit[] = [
  { id: '1', nom: 'Notebook Pro', prix: 4500, stock: 10 },
  { id: '2', nom: 'Souris Gamer', prix: 250, stock: 50 },
  { id: '3', nom: 'Clavier Mécanique', prix: 450, stock: 30 }
];

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const categorie = searchParams.get('categorie');
  const limite = parseInt(searchParams.get('limite') || '10');

  let resultat = [...produits];

  if (categorie) {
    // Filtrer par catégorie si nécessaire
  }

  resultat = resultat.slice(0, limite);

  return NextResponse.json({
    succes: true,
    donnees: resultat,
    total: resultat.length
  });
}

export async function POST(request: NextRequest) {
  try {
    const donnees = await request.json();

    // Validations
    if (!donnees.nom || !donnees.prix) {
      return NextResponse.json(
        { succes: false, erreur: 'Le nom et le prix sont obligatoires' },
        { status: 400 }
      );
    }

    const nouveauProduit: Produit = {
      id: Date.now().toString(),
      nom: donnees.nom,
      prix: donnees.prix,
      stock: donnees.stock || 0
    };

    produits.push(nouveauProduit);

    return NextResponse.json(
      { succes: true, donnees: nouveauProduit },
      { status: 201 }
    );
  } catch (error) {
    return NextResponse.json(
      { succes: false, erreur: 'Erreur lors du traitement de la requête' },
      { status: 500 }
    );
  }
}

Edge Functions pour la Personnalisation

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export const config = {
  matcher: ['/api/:path*', '/dashboard/:path*']
};

export function middleware(request: NextRequest) {
  // Détecter le pays de l'utilisateur via le header
  const pays = request.geo?.country || 'FR';
  const ville = request.geo?.city || 'Unknown';

  // Ajouter des headers de localisation
  const response = NextResponse.next();
  response.headers.set('x-user-country', pays);
  response.headers.set('x-user-city', ville);

  // Rate limiting simple basé sur l'IP
  const ip = request.ip || 'unknown';
  const rateLimitKey = `rate-limit:${ip}`;

  // En production, utilisez un service comme Upstash Redis
  // pour le rate limiting distribué

  // Log de la requête
  console.log(`[${new Date().toISOString()}] ${request.method} ${request.url} - ${pays}/${ville}`);

  return response;
}

// app/api/personnalisation/route.ts
import { NextRequest, NextResponse } from 'next/server';

export const runtime = 'edge'; // Exécuter à l'edge

export async function GET(request: NextRequest) {
  const pays = request.headers.get('x-user-country') || 'FR';

  // Personnalisation basée sur la localisation
  const configurations = {
    FR: {
      devise: 'EUR',
      langue: 'fr-FR',
      formatDate: 'dd/MM/yyyy',
      offres: ['livraison-gratuite-france', 'reduction-premier-achat']
    },
    US: {
      devise: 'USD',
      langue: 'en-US',
      formatDate: 'MM/dd/yyyy',
      offres: ['free-shipping', 'black-friday']
    }
  };

  const config = configurations[pays as keyof typeof configurations] || configurations.FR;

  return NextResponse.json({
    succes: true,
    donnees: config,
    traiteA: 'edge',
    region: request.geo?.region || 'unknown'
  });
}

Patterns et Meilleures Pratiques

Cold Start : Minimiser la Latence

Le cold start est le temps nécessaire pour qu'une fonction s'initialise à la première exécution. Voici des stratégies pour le minimiser :

// ÉVITEZ : imports dynamiques dans le handler
export const handler = async () => {
  const { DynamoDBClient } = await import('@aws-sdk/client-dynamodb'); // Cold start !
  // ...
};

// PRÉFÉREZ : imports en haut du fichier
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// Initialisez les connexions en dehors du handler (réutilisées entre les invocations)
const client = new DynamoDBClient({});

export const handler = async () => {
  // client est déjà initialisé
};

Structure de Projet Recommandée

projet-serverless/
├── src/
│   ├── handlers/           # Fonctions Lambda
│   │   ├── utilisateurs.ts
│   │   ├── produits.ts
│   │   └── commandes.ts
│   ├── lib/                # Utilitaires partagés
│   │   ├── dynamodb.ts
│   │   ├── s3.ts
│   │   └── validation.ts
│   ├── types/              # Types TypeScript
│   │   └── index.ts
│   └── middlewares/        # Middlewares personnalisés
│       └── authentification.ts
├── tests/
│   ├── unit/
│   └── integration/
├── serverless.yml
├── package.json
└── tsconfig.json

Traitement des Erreurs Robuste

// src/lib/erreurs.ts
export class ErreurApplication extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500,
    public code?: string
  ) {
    super(message);
    this.name = 'ErreurApplication';
  }
}

export class ErreurValidation extends ErreurApplication {
  constructor(message: string) {
    super(message, 400, 'ERREUR_VALIDATION');
  }
}

export class ErreurNonTrouve extends ErreurApplication {
  constructor(ressource: string) {
    super(`${ressource} non trouvé`, 404, 'NON_TROUVE');
  }
}

export class ErreurNonAutorise extends ErreurApplication {
  constructor() {
    super('Non autorisé', 401, 'NON_AUTORISE');
  }
}

// src/lib/handler-wrapper.ts
import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import { ErreurApplication } from './erreurs';

type HandlerFn = (event: any, context: any) => Promise<any>;

export const wrapHandler = (fn: HandlerFn): APIGatewayProxyHandler => {
  return async (event, context): Promise<APIGatewayProxyResult> => {
    try {
      const resultat = await fn(event, context);

      return {
        statusCode: 200,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ succes: true, donnees: resultat })
      };
    } catch (error) {
      console.error('Erreur dans le handler :', error);

      if (error instanceof ErreurApplication) {
        return {
          statusCode: error.statusCode,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            succes: false,
            erreur: error.message,
            code: error.code
          })
        };
      }

      return {
        statusCode: 500,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          succes: false,
          erreur: 'Erreur interne du serveur'
        })
      };
    }
  };
};

// Utilisation :
import { wrapHandler } from '../lib/handler-wrapper';
import { ErreurValidation, ErreurNonTrouve } from '../lib/erreurs';

export const chercherProduit = wrapHandler(async (event) => {
  const { id } = event.pathParameters;

  if (!id) {
    throw new ErreurValidation('L\'ID du produit est obligatoire');
  }

  const produit = await chercherProduitEnBase(id);

  if (!produit) {
    throw new ErreurNonTrouve('Produit');
  }

  return produit;
});

Quand Utiliser (et Quand Éviter) le Serverless

Cas d'Usage Idéaux

Utilisez le serverless quand :

  • Charges de travail imprévisibles ou avec des pics
  • APIs REST/GraphQL
  • Traitement d'événements (webhooks, files d'attente)
  • Automatisations et tâches planifiées
  • Prototypage rapide
  • Projets avec budget limité

Quand Considérer des Alternatives

Évitez le serverless quand :

  • Traitement de longue durée (>15 minutes)
  • Applications nécessitant une latence ultra-basse constante
  • Workloads avec haute utilisation constante (peut être plus cher)
  • Besoin d'état persistant en mémoire

Comparatif de Coûts

Scénario : API avec 1 million de requêtes/mois

Fournisseur Coût Estimé
AWS Lambda ~0,20 € (128MB, 100ms)
Vercel Pro Inclus dans le plan 20 €/mois
Google Cloud Functions ~0,40 €
Azure Functions ~0,20 €

Les coûts réels varient avec la mémoire, le temps d'exécution et le transfert de données.

Monitoring et Observabilité

Pour la production, il est essentiel d'avoir de la visibilité sur vos fonctions :

Outils recommandés :

  • AWS CloudWatch - Logs et métriques natives
  • Datadog - APM complet pour serverless
  • Lumigo - Debugging visuel de serverless
  • Sentry - Tracking d'erreurs

Si vous voulez approfondir la performance des applications web, je vous recommande de consulter l'article Web APIs Modernes du Navigateur qui complète bien les concepts de serverless.

C'est parti ! 🦅

Commentaires (0)

Cet article n'a pas encore de commentaires. Soyez le premier!

Ajouter des commentaires