Tous les tutoriels serverless partagent le même premier acte sans joie. Installer le CLI. Créer un rôle IAM. Écrire un fichier de stack qui provisionne une table DynamoDB. Écrire un deuxième fichier qui autorise une Lambda à lire cette table. Écrire un troisième fichier qui relie une route API Gateway à la Lambda. Générer un SDK client. Configurer l'URL du endpoint dans le .env du frontend. Puis, quarante minutes plus tard, écrire les six lignes de logique métier que vous vouliez réellement écrire.
AWS Blocks, qu'AWS a mis en préversion publique le 16 juin 2026, supprime ce premier acte. C'est un framework TypeScript open source où une seule ligne — new DistributedTable(scope, 'todos', { schema, key }) — est simultanément la définition d'infrastructure, l'API runtime et une implémentation locale qui tourne en mémoire sans aucun compte AWS. La même ligne devient une table DynamoDB avec ses GSI au moment du déploiement, et un appel AWS SDK à l'intérieur de Lambda à l'exécution.
L'astuce, ce sont les exports conditionnels de Node.js. Quand vous importez un Block, le résolveur de modules vous renvoie un fichier différent selon le contexte d'exécution : un mock en mémoire pendant npm run dev, un construct CDK pendant la synthèse, un wrapper SDK dans Lambda. Vous ne configurez jamais ce mécanisme. Vous ne le voyez jamais. Vous écrivez du code applicatif, et l'infrastructure en est déduite.
Ce tutoriel construit une vraie application : un gestionnaire de tâches authentifié avec requêtes structurées, mises à jour WebSocket en direct, pièces jointes, tâche de nettoyage nocturne et assistant IA adossé à Amazon Bedrock. Tout tourne d'abord sur votre machine. Le déploiement est le dernier chapitre, et il tient en trois commandes.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 22 ou plus récent et npm 10 ou plus récent. AWS Blocks dépend de
Array.fromAsyncet de la résolution moderne des exports conditionnels : les versions plus anciennes échouent de façon déroutante. Vérifiez avecnode --version. - Une bonne connaissance de TypeScript. Vous devez être à l'aise avec les génériques et les itérateurs asynchrones. Vous n'avez pas besoin de connaître AWS CDK, CloudFormation ou la modélisation DynamoDB.
- Un éditeur avec support TypeScript — VS Code, Kiro, ou n'importe quel éditeur avec un serveur de langage TypeScript. Une grande partie de la valeur ici tient à l'IntelliSense qui circule du backend jusqu'au frontend.
- Optionnel, uniquement pour le chapitre déploiement : un compte AWS, l'AWS CLI v2 configurée, et CDK bootstrappé. Les étapes 1 à 7 n'en ont aucun besoin.
Les étapes 1 à 7 ne nécessitent aucun compte AWS, aucun identifiant et aucune connexion Internet. Vous pouvez terminer l'application entière hors ligne. AWS n'entre en jeu qu'à l'étape 8.
Ce que vous allez construire
Un gestionnaire de tâches multi-utilisateurs où chaque utilisateur connecté dispose de tâches privées, voit les mises à jour poussées en direct entre ses onglets, peut joindre un fichier à une tâche, voit ses tâches obsolètes archivées automatiquement chaque nuit, et peut demander à un assistant IA de résumer sa charge de travail.
Traduit en Blocks, cela donne :
| Fonctionnalité | Block | En local | Sur AWS |
|---|---|---|---|
| Connexion identifiant/mot de passe | AuthBasic | JWT local | Enregistrements DynamoDB |
| Stockage et requêtes de tâches | DistributedTable | En mémoire | DynamoDB + GSI |
| API typée | ApiNamespace | Serveur HTTP local | API Gateway + Lambda |
| Mises à jour en direct | Realtime | EventEmitter | API Gateway WebSocket |
| Pièces jointes | FileBucket | Dossier .bb-data/ | S3 avec URL présignées |
| Archivage nocturne | CronJob | Timers Node | EventBridge Scheduler |
| Assistant IA | Agent | Fournisseur simulé | Amazon Bedrock |
Sept capacités backend, un fichier, zéro YAML.
Étape 1 : initialiser le projet
AWS Blocks fournit un package create. Lancez-le :
npm create @aws-blocks/blocks-app@latest task-tracker
cd task-tracker
npm installVous obtenez une arborescence volontairement réduite :
task-tracker/
├── aws-blocks/
│ └── index.ts # le backend : Blocks + définitions d'API
├── src/
│ └── index.ts # le frontend
├── index.html
└── package.json
Deux répertoires. aws-blocks/index.ts est ce que la documentation appelle la couche IFC — Infrastructure From Code. C'est à la fois votre définition d'infrastructure et votre runtime backend. src/ est un frontend classique qui importe le backend directement.
D'autres templates sont disponibles via --template : nextjs, react, auth-cognito, demo, bare, backend et amplify. Lancer la commande dans un projet existant — en passant . comme répertoire — y ajoute un backend aws-blocks/, et le CLI détecte automatiquement un projet Amplify Gen 2 s'il en existe un.
Démarrez le serveur de développement :
npm run devOuvrez http://localhost:3000. Une application de todos fonctionnelle avec inscription, connexion et CRUD tourne déjà — entièrement en processus, sans aucune connexion cloud. Le rechargement à chaud est actif.
Étape 2 : comprendre Scope et Blocks
Ouvrez aws-blocks/index.ts. Tout commence par un Scope :
import { Scope, KVStore } from '@aws-blocks/blocks';
const scope = new Scope('task-tracker');
const cache = new KVStore(scope, 'cache', {});
const sessions = new KVStore(scope, 'sessions', {});
// Identifiants complets : task-tracker/cache et task-tracker/sessionsUn Scope est un conteneur de noms. Chaque Block doit être créé à l'intérieur d'un scope, et son identifiant complet dérive du nom du scope plus l'ID passé en deuxième argument. Cet identifiant détermine le nom de la ressource AWS physique.
Les ID de Blocks sont définitifs. Renommer le deuxième argument du constructeur après un déploiement pousse CloudFormation à supprimer puis recréer la ressource sous-jacente. Pour les Blocks avec état — KVStore, DistributedTable, Database, FileBucket — cela signifie une perte de données irréversible. Traitez les ID de Blocks comme immuables dès qu'ils sont livrés dans un environnement qui compte.
La même instance KVStore expose une API identique dans les trois contextes :
await cache.put('user:123', { name: 'Alice' });
const user = await cache.get('user:123');En local, cela écrit dans un répertoire .bb-data/ à la racine du projet. Dans Lambda, c'est un PutItem DynamoDB. Votre code ne le sait pas et n'a pas à le savoir.
Étape 3 : modéliser les tâches avec DistributedTable
KVStore gère les recherches quand vous connaissez toujours la clé. Notre gestionnaire de tâches doit interroger par utilisateur et par statut : nous voulons donc DistributedTable, un stockage structuré validé par schéma avec index secondaires.
Remplacez le contenu de aws-blocks/index.ts :
import { ApiNamespace, Scope, DistributedTable, AuthBasic } from '@aws-blocks/blocks';
import { z } from 'zod';
const scope = new Scope('task-tracker');
const auth = new AuthBasic(scope, 'auth', {
sessionDuration: 86400,
passwordPolicy: { minLength: 8, requireDigits: true },
});
export const authApi = auth.createApi();
const taskSchema = z.object({
userId: z.string(),
taskId: z.string(),
title: z.string().min(1).max(200),
status: z.enum(['open', 'done', 'archived']),
createdAt: z.string(),
attachmentPath: z.string().optional(),
});
const tasks = new DistributedTable(scope, 'tasks', {
schema: taskSchema,
key: { partitionKey: 'userId', sortKey: 'taskId' },
indexes: {
byStatus: { partitionKey: 'status', sortKey: 'createdAt' },
},
});Trois choses viennent de se produire. Le schéma Zod est devenu à la fois les types à la compilation et la validation à l'exécution. La configuration key est devenue la clé primaire DynamoDB. Le bloc indexes est devenu un Global Secondary Index — et la modélisation des GSI DynamoDB, habituellement la partie la plus ardue d'un projet serverless, tient désormais en quatre mots.
Ne passez jamais un seul argument de type explicite à DistributedTable. Écrire new DistributedTable<Task>(...) fige uniquement le type de l'élément et laisse les génériques de clé et d'index retomber sur leurs valeurs par défaut trop larges, ce qui casse l'inférence de clé : get() exigera alors tous les champs de votre type au lieu des seuls champs de clé. Laissez tous les génériques s'inférer, ou passez-les tous les trois. Ajouter as const ne corrige rien.
Sémantique des requêtes
query() retourne un AsyncIterable, pas un tableau. C'est délibéré : cela correspond directement au modèle de requêtes paginées de DynamoDB, donc vous obtenez la pagination automatique sans écrire de boucle de curseur.
// Requête sur la clé primaire — notez les opérateurs `where` typés
for await (const task of tasks.query({
where: { userId: { equals: 'alice' }, taskId: { beginsWith: '2026-' } },
})) {
console.log(task.title);
}
// Requête sur le GSI
for await (const task of tasks.query({
index: 'byStatus',
where: { status: { equals: 'open' } },
limit: 25,
order: 'desc',
})) {
console.log(task.title);
}Pour collecter les résultats d'un coup, utilisez await Array.fromAsync(tasks.query({ ... })). Préférez query() à scan() — un scan lit chaque élément de la table.
Les méthodes de données sont réservées au runtime. Appeler tasks.put(), tasks.query(), rt.publish() et consorts au niveau supérieur de aws-blocks/index.ts déclenche tasks.put is not a function. Le code de niveau supérieur s'exécute pendant la synthèse CDK, où le Block se résout en son construct d'infrastructure et n'a aucune méthode de données. Construire un Block au niveau du module est parfaitement valide — seuls les appels de méthodes doivent vivre dans un handler. Pour injecter des données initiales, faites-le depuis une méthode d'API ou un script runtime distinct.
Étape 4 : exposer une API typée avec ApiNamespace
ApiNamespace est le pont entre le navigateur et le backend. C'est du RPC, pas du REST : vous définissez des méthodes, le frontend les appelle, TypeScript vérifie les deux côtés. Il n'y a aucune étape de génération de code ni fichier OpenAPI.
Ajoutez à aws-blocks/index.ts :
export const api = new ApiNamespace(scope, 'api', (context) => ({
async createTask(title: string) {
const user = await auth.requireAuth(context);
const task = {
userId: user.username,
taskId: crypto.randomUUID(),
title,
status: 'open' as const,
createdAt: new Date().toISOString(),
};
await tasks.put(task);
return task;
},
async listTasks() {
const user = await auth.requireAuth(context);
return await Array.fromAsync(
tasks.query({ where: { userId: { equals: user.username } } })
);
},
async completeTask(taskId: string) {
const user = await auth.requireAuth(context);
const task = await tasks.get({ userId: user.username, taskId });
if (!task) throw new Error('Task not found');
const updated = { ...task, status: 'done' as const };
await tasks.put(updated);
return updated;
},
async deleteTask(taskId: string) {
const user = await auth.requireAuth(context);
await tasks.delete({ userId: user.username, taskId });
},
}));
export { auth };Le paramètre context est un BlocksContext — l'objet par requête qui porte les en-têtes et les cookies. Vous ne le construisez jamais : le framework en fournit un par requête entrante. Les Blocks d'authentification l'utilisent pour lire le cookie de session, ce qui explique que auth.requireAuth(context) soit la ligne unique qui sécurise une méthode. Elle lève une SessionExpiredException 401 quand aucune session valide n'existe, donc un appel non authentifié n'atteint jamais votre couche de stockage.
Notez le motif multi-tenant : user.username est la clé de partition sur chaque lecture et chaque écriture. Un utilisateur ne peut physiquement pas interroger les lignes d'un autre, parce que la clé de partition dérive de la session vérifiée et non d'un argument fourni par le client.
L'appeler depuis le frontend
Dans src/index.ts :
import { api, authApi } from 'aws-blocks';
const task = await api.createTask('Publier le tutoriel');
console.log(task.taskId);
const all = await api.listTasks();C'est toute l'intégration côté client. Pas d'URL de base, pas de wrapper fetch, pas de SDK généré, pas de plomberie d'en-tête Authorization. Ajoutez un second argument à createTask dans le backend et votre frontend affiche une erreur de compilation avant même que vous ayez rechargé le navigateur.
Étape 5 : pousser des mises à jour en direct avec Realtime
Deux onglets de navigateur doivent rester synchronisés. Realtime fournit un pub/sub WebSocket typé avec des charges utiles validées par Zod.
import { Realtime } from '@aws-blocks/blocks';
const rt = new Realtime(scope, 'live', {
namespaces: {
tasks: Realtime.namespace(
z.object({
event: z.enum(['created', 'completed', 'deleted']),
taskId: z.string(),
title: z.string(),
})
),
},
});La publication se fait côté serveur, à l'intérieur d'une méthode d'API — c'est-à-dire exactement là où votre logique d'autorisation vit déjà :
async createTask(title: string) {
const user = await auth.requireAuth(context);
const task = { /* ... comme ci-dessus ... */ };
await tasks.put(task);
await rt.publish('tasks', user.username, {
event: 'created',
taskId: task.taskId,
title,
});
return task;
},Le deuxième argument est le canal. Utiliser user.username comme nom de canal donne à chaque utilisateur un flux privé au sein de l'espace de noms partagé tasks.
Les handles de canal sont délibérément en lecture seule — ils n'ont pas de méthode publish() — donc un client ne peut jamais diffuser directement. Le motif recommandé consiste à conditionner l'abonnement à une méthode d'API qui ne retourne le handle qu'après vérification des permissions :
async subscribeToMyTasks() {
const user = await auth.requireAuth(context);
return rt.getChannel('tasks', user.username);
},Côté client, abonnez-vous et attendez toujours established avant de vous fier à la connexion. Cette promesse se résout une fois la poignée de main WebSocket et l'autorisation côté serveur terminées, et échoue en cas de refus d'authentification :
const channel = await api.subscribeToMyTasks();
const sub = channel.subscribe((msg) => {
console.log(`${msg.event}: ${msg.title}`);
refreshTaskList();
});
await sub.established;
// plus tard
sub.unsubscribe();En local, tout cela tourne sur un EventEmitter en processus avec un serveur WebSocket local. Sur AWS, cela devient une API WebSocket API Gateway avec gestion des connexions dans DynamoDB. Realtime est dimensionné pour des canaux comptant de quelques dizaines à quelques milliers d'abonnés simultanés — la latence de publication croît à peu près linéairement, autour de 100 ms pour 1 000 abonnés. Au-delà de 10 000 abonnés par canal, il vous faut un fan-out shardé explicite.
Étape 6 : pièces jointes et travaux planifiés
FileBucket
Les pièces jointes ne devraient jamais transiter par votre Lambda. FileBucket émet des URL présignées pour que le navigateur téléverse directement vers S3 :
import { FileBucket } from '@aws-blocks/blocks';
const attachments = new FileBucket(scope, 'attachments', {
corsRules: [
{
allowedOrigins: ['http://localhost:3000'],
allowedMethods: ['GET', 'PUT'],
allowedHeaders: ['*'],
},
],
lifecycleRules: [{ prefix: 'tmp/', expirationDays: 7 }],
removalPolicy: 'destroy',
});Puis dans l'espace de noms d'API :
async getUploadUrl(taskId: string, fileName: string) {
const user = await auth.requireAuth(context);
const path = `${user.username}/${taskId}/${fileName}`;
const url = await attachments.putUrl(path);
return { url, path };
},
async attachFile(taskId: string, path: string) {
const user = await auth.requireAuth(context);
const task = await tasks.get({ userId: user.username, taskId });
if (!task) throw new Error('Task not found');
await tasks.put({ ...task, attachmentPath: path });
},
async getDownloadUrl(taskId: string) {
const user = await auth.requireAuth(context);
const task = await tasks.get({ userId: user.username, taskId });
if (!task?.attachmentPath) return null;
return attachments.getUrl(task.attachmentPath);
},Notez que getUrl() et get() retournent null pour un fichier absent au lieu de lever une exception — vérifiez explicitement la valeur nulle. En local, les fichiers atterrissent dans .bb-data/ sur votre système de fichiers, en reproduisant le comportement de l'API S3 d'assez près pour que les flux d'URL présignées fonctionnent à l'identique.
Ne définissez removalPolicy: 'destroy' que sur les stacks de bac à sable et éphémères. Le comportement par défaut de CDK pour les buckets S3 est RETAIN, et c'est celui que vous voulez en production : il empêche un npm run destroy accidentel de supprimer les données de vos utilisateurs.
CronJob
Archivez tout ce qui reste ouvert depuis plus de 30 jours :
import { CronJob } from '@aws-blocks/blocks';
const archiveStale = new CronJob(scope, 'archive-stale', {
schedule: 'cron(0 3 * * ? *)',
timezone: 'Africa/Tunis',
description: 'Archiver les tâches ouvertes depuis plus de 30 jours',
handler: async (event) => {
const cutoff = new Date(Date.now() - 30 * 86400_000).toISOString();
for await (const task of tasks.query({
index: 'byStatus',
where: { status: { equals: 'open' }, createdAt: { lessThan: cutoff } },
})) {
await tasks.put({ ...task, status: 'archived' });
}
},
});Les expressions cron d'EventBridge comptent six champs — cron(minute heure jour-du-mois mois jour-de-semaine année) — et l'un des deux champs jour-du-mois ou jour-de-semaine doit valoir ?. Pour de simples intervalles, les expressions de fréquence sont plus lisibles : rate(5 minutes), rate(1 hour), rate(7 days). Les handlers cron doivent être idempotents : EventBridge garantit une livraison au moins une fois, donc une double invocation ne doit pas corrompre l'état. La boucle ci-dessus est sûre car passer status à 'archived' deux fois revient à le faire une seule fois.
AsyncJob
Pour du travail à oublier déclenché par une action utilisateur plutôt que par une horloge, utilisez AsyncJob :
import { AsyncJob } from '@aws-blocks/blocks';
const notify = new AsyncJob(scope, 'notify', {
schema: z.object({ to: z.string().email(), taskTitle: z.string() }),
maxRetries: 3,
handler: async (payload, ctx) => {
console.log(`Job ${ctx.jobId}, tentative ${ctx.receiveCount}`);
await sendEmail(payload.to, `Tâche terminée : ${payload.taskTitle}`);
},
});
// Depuis une méthode d'API — retourne immédiatement
const { jobId } = await notify.submit({ to: user.username, taskTitle: title });submit() rend la main dès que le message est mis en file, donc la réponse de l'API n'est pas bloquée. Sur AWS, cela provisionne une file SQS plus une file de lettres mortes ; en local, tout s'exécute en processus via setTimeout, avec les réessais, le comportement DLQ et la limite de 256 Ko de charge utile appliqués à l'identique. Utilisez submitBatch() pour jusqu'à 10 charges utiles d'un coup.
Étape 7 : ajouter un assistant IA
Le Block Agent s'appuie sur le SDK Strands Agents et vous donne le streaming, l'appel d'outils, l'approbation humaine dans la boucle et la persistance des conversations.
import { Agent, BedrockModels } from '@aws-blocks/blocks';
const assistant = new Agent(scope, 'assistant', {
model: { deployed: BedrockModels.BALANCED },
systemPrompt:
"Tu aides les utilisateurs à gérer leur liste de tâches. Sois concis. " +
"Utilise l'outil listOpenTasks avant de répondre aux questions sur la charge de travail.",
streamingMode: 'token',
tools: (tool) => ({
listOpenTasks: tool({
description: "Lister les tâches ouvertes de l'utilisateur courant",
parameters: z.object({ userId: z.string() }),
execute: async ({ userId }) =>
Array.fromAsync(
tasks.query({
where: { userId: { equals: userId } },
})
),
}),
}),
});BedrockModels.BALANCED correspond actuellement à Claude Sonnet 4.6 et constitue le choix par défaut recommandé ; BedrockModels.SMART correspond à Claude Opus 4.8 pour les raisonnements les plus exigeants. Ce sont des préréglages nommés par capacité, donc le modèle sous-jacent peut être mis à niveau sans modifier votre code. Ils utilisent des profils d'inférence globaux, ce qui signifie que les requêtes peuvent être routées vers n'importe quelle région supportée — si vous avez des exigences de résidence des données, spécifiez explicitement un profil d'inférence limité à une région.
En local, l'Agent utilise un fournisseur simulé basé sur des mots-clés : réponses prévisibles, aucune clé d'API, aucun coût, aucun réseau. Cela rend le code d'agent testable en CI. Pour tester avec un vrai modèle en local, pointez-le vers Ollama ou tout endpoint compatible OpenAI :
model: {
deployed: BedrockModels.BALANCED,
local: {
provider: 'openai-api',
modelId: 'llama3.1:8b',
endpoint: 'http://localhost:11434/v1',
apiKey: 'ollama',
},
},Streamer correctement
stream() soumet le message via AsyncJob et rend la main immédiatement — aucun risque de timeout API Gateway sur les exécutions d'agent longues — puis publie les fragments sur un canal Realtime. Abonnez-vous avant d'envoyer, sinon vous perdrez les premiers tokens :
const conversationId = await assistant.createConversationId(userId);
const channel = await assistant.getChannel(conversationId);
const sub = channel.subscribe((chunk) => {
if (chunk.type === 'text-delta') appendToUI(chunk.text);
if (chunk.type === 'tool-call') showSpinner(chunk.toolName);
if (chunk.type === 'done') finish(chunk.text, chunk.usage);
});
await sub.established;
const result = await assistant.stream('Sur quoi devrais-je travailler aujourd\'hui ?', {
conversationId,
userId,
});
const done = await result.complete();Le Block Agent n'autorise pas les lectures. getConversation(id) et getPendingInterrupts(id) ne prennent qu'un identifiant — tout appelant disposant d'un UUID de conversation valide récupère les messages. L'autorisation est votre responsabilité dans le handler d'API : dérivez userId de la session, appelez listConversations(userId), et vérifiez que la conversation appartient bien à cet utilisateur avant de retourner quoi que ce soit. deleteConversation(id, userId) est en revanche restreint au propriétaire en interne et reste sûr.
Étape 8 : déployer sur AWS
Maintenant, et seulement maintenant, vous avez besoin d'un compte AWS.
Configuration unique — configurez l'AWS CLI, vérifiez-la, et bootstrappez CDK pour votre compte et votre région :
aws sts get-caller-identity
npx cdk bootstrap aws://123456789012/eu-west-1Le bootstrap n'est requis qu'une seule fois par couple compte/région.
Déployez ensuite dans un bac à sable — un environnement éphémère, propre à chaque développeur, qui utilise le hot-swapping Lambda plutôt que des mises à jour CloudFormation complètes :
npm run sandboxCela prend quelques secondes, pas quelques minutes, et chaque Block se résout désormais en services AWS réels : tables DynamoDB, endpoint API Gateway, fonction Lambda, file SQS, planification EventBridge, bucket S3. Votre code applicatif est identique octet pour octet à celui qui tournait en local.
Les bacs à sable comptent parce que les implémentations locales sont fidèles mais pas parfaites. Il vaut la peine de tester contre les services réels : les performances des requêtes DynamoDB sur des volumes réalistes, les limites de permissions IAM, le comportement CORS de S3 depuis une vraie origine navigateur, et la sortie réelle du modèle Bedrock par rapport au fournisseur simulé.
Pour la préproduction ou la production, lancez un déploiement CloudFormation complet :
npm run deployCommandes de démantèlement :
npm run sandbox:destroy # supprimer votre bac à sable éphémère
npm run destroy # supprimer le déploiement completÉtape 9 : s'échapper vers CDK quand c'est nécessaire
Les frameworks d'infrastructure-from-code échouent en général à la frontière : dès que vous avez besoin d'une ressource que le framework ne modélise pas, vous êtes coincé. AWS Blocks règle cela avec une couche CDK optionnelle dans aws-blocks/index.cdk.ts. Si vous ne créez jamais ce fichier, il en génère un pour vous.
// aws-blocks/index.cdk.ts
import * as cdk from 'aws-cdk-lib';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { BlocksStack } from '@aws-blocks/blocks/cdk';
const app = new cdk.App();
const stack = await BlocksStack.create(app, 'task-tracker-stack', {
backendHandlerPath: './index.handler.ts',
backendCDKPath: './index.ts',
});
const queue = new sqs.Queue(stack, 'legacy-queue');
queue.grantSendMessages(stack.handler);
stack.handler.addEnvironment('QUEUE_URL', queue.queueUrl);Vous récupérez le Stack CDK brut et la Lambda backend via stack.handler, donc n'importe quel construct de l'écosystème CDK est disponible — domaines personnalisés, configuration VPC, topics SNS, ressources existantes. Il n'y a aucun mur contre lequel se cogner.
Le même principe vaut pour l'adoption d'une infrastructure existante. KVStore.fromExisting(tableName), DistributedTable.fromExisting(tableName) et FileBucket.fromExisting(bucketName) enveloppent des ressources que vous possédez déjà au lieu d'en provisionner de nouvelles — vous pouvez donc placer une API Blocks devant une table DynamoDB de production sans rien migrer.
Tester votre implémentation
Vérifiez en local, dans cet ordre :
- Frontière d'authentification. Appelez
api.listTasks()avant de vous connecter. Vous devez obtenir uneSessionExpiredException401, pas un tableau vide. Si vous obtenez un tableau vide, un handler a oubliérequireAuth. - Isolation des locataires. Inscrivez deux utilisateurs, créez des tâches avec chacun, et confirmez qu'aucun ne voit les lignes de l'autre.
- Validation de schéma. Appelez
api.createTask('')et confirmez que la contrainte Zodmin(1)la rejette. La validation s'exécute côté serveur, pas seulement dans le navigateur. - Diffusion temps réel. Ouvrez deux onglets connectés avec le même utilisateur. Créer une tâche dans l'un doit apparaître dans l'autre sans rafraîchissement.
- Sûreté de typage. Ajoutez un paramètre à une méthode backend et confirmez que votre frontend ne compile plus avant de l'exécuter. C'est la propriété pour laquelle tout ce framework existe.
- Persistance. Redémarrez
npm run dev. Les donnéesKVStoreetFileBucketsurvivent dans.bb-data/;DistributedTableest en mémoire en local et ne survit pas.
Dépannage
tasks.put is not a function — Vous avez appelé une méthode de données au niveau supérieur de aws-blocks/index.ts. Ce code s'exécute pendant la synthèse CDK, où le Block est un construct d'infrastructure sans méthodes de données. Déplacez l'appel dans une méthode d'API, un handler de job ou un script runtime.
get() exige tous les champs de mon type — Vous avez passé un générique explicite unique : new DistributedTable<Task>(...). Retirez-le et laissez l'inférence faire son travail, ou fournissez les trois génériques.
ZodType missing properties — Le Block Agent requiert Zod 4 en peer dependency. Cherchez un Zod dupliqué ou plus ancien dans votre lockfile.
Les premiers tokens de l'agent manquent — Vous avez appelé stream() avant de vous abonner. Abonnez-vous, attendez sub.established, puis envoyez.
Ressource recréée et données perdues après déploiement — Un ID de Block a changé. Les ID de Blocks sont l'identité de la ressource ; en renommer un équivaut à supprimer puis recréer. Restaurez depuis une sauvegarde et rétablissez l'ID.
Erreurs de version Node — AWS Blocks exige Node 22+. Lancez nvm use 22.
AWS Blocks face à Amplify, SST et Encore
Blocks n'est pas Amplify Gen 2 rebaptisé. Amplify est une plateforme d'hébergement et de backend avec une console ; Blocks est une bibliothèque qui génère du CDK, sans console ni plan de contrôle managé — et le CLI s'intègre explicitement à un projet Amplify Gen 2 existant au lieu de le remplacer.
Comparé à SST Ion, Blocks échange de l'étendue contre une histoire locale bien plus solide : le mode dev de SST fait transiter vers des ressources AWS réellement déployées, alors que Blocks exécute de véritables implémentations locales sans aucun compte cloud. Comparé à Encore.ts, la philosophie RPC et infrastructure déduite est proche, mais Blocks émet du CDK standard que vous pouvez lire, étendre et, à terme, quitter. Comparé à Alchemy, les deux répondent à la même question sur des clouds différents.
Le cadrage retenu par AWS au lancement est que Blocks est conçu pour que les agents de codage IA produisent des backends corrects du premier coup : la surface d'API est réduite, la nomenclature est sans ambiguïté, et il n'existe aucun moyen d'écrire une Lambda qui n'aurait pas la permission de lire sa propre table. Que vous codiez ou non avec un agent, cette contrainte produit un framework agréable pour les humains aussi.
Aller plus loin
- Remplacez
AuthBasicparAuthCognitoavant la production — vous obtenez MFA, SAML, connexion sociale et passkeys, etAuthBasicest explicitement limité aux prototypes et outils internes. - Remplacez
DistributedTableparDatabasesi vos schémas d'accès nécessitent des JOIN. Il utilise PGlite en local et Aurora Serverless v2 sur AWS, avec Kysely pour du SQL typé. - Ajoutez
KnowledgeBaseaux côtés d'Agentpour un pipeline RAG sur vos propres documents. - Branchez
Logger,MetricsetTracer, puis ajoutezDashboardpour générer automatiquement un tableau de bord CloudWatch à partir de vos définitions de métriques. - Générez des clients natifs. Un
blocks.spec.jsonproduit des clients typés Kotlin Multiplatform, Swift et Dart qui appellent le même backend via JSON-RPC. - Lectures liées : créer des serveurs MCP en TypeScript, Cloudflare Workflows pour l'exécution durable et les workflows durables Temporal.
Conclusion
AWS Blocks est le premier framework d'infrastructure-from-code issu d'AWS lui-même qui ne ressemble pas à un piège. L'histoire locale est authentiquement locale — pas de compte, pas d'identifiants, pas de réseau — ce qui change la façon dont une équipe intègre un nouveau développeur et dont la CI s'exécute. La sûreté de typage repose sur une véritable inférence de bout en bout, pas sur une génération de code qu'il faut penser à relancer. Et l'échappatoire CDK signifie que les opinions du framework sont un point de départ plutôt qu'un plafond.
Les réserves de la préversion sont réelles : les ID de Blocks sont impitoyables, AuthBasic n'est pas une authentification de production, le Block Agent vous laisse l'autorisation des lectures, et la surface d'API bougera avant la disponibilité générale. Mais le pari central — que la ligne de code qui crée une table devrait être la table — est celui autour duquel AWS tourne depuis une décennie, et c'est ce qui s'en approche le plus à ce jour.
Commencez par npm create @aws-blocks/blocks-app@latest. Vous aurez un backend authentifié fonctionnel avant d'avoir fini de décider s'il vous plaît.