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.

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.jsonTraitement 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.

