Déployer Next.js sur AWS avec SST Ion : Guide Complet Serverless

Noqta Team
Par Noqta Team ·

Chargement du lecteur de synthèse vocale...

Introduction

Déployer une application Next.js sur AWS signifiait traditionnellement assembler CloudFront, Lambda@Edge, S3 et une pile de fichiers YAML Terraform ou CloudFormation. Ça fonctionne — mais c'est lent, fragile et pénible à itérer.

SST Ion (v3) change la donne. C'est un framework open-source qui vous permet de définir toute votre infrastructure AWS en TypeScript, directement aux côtés de votre code applicatif. Un seul fichier, un seul langage, zéro YAML. Il déploie votre application Next.js sur CloudFront + S3 + Lambda en utilisant OpenNext sous le capot, vous offrant une configuration serverless prête pour la production en une seule commande.

Dans ce tutoriel, vous apprendrez à :

  • Initialiser SST dans un projet Next.js
  • Déployer sur AWS avec CloudFront, S3 et Lambda
  • Ajouter le téléchargement de fichiers S3 avec des URLs présignées
  • Intégrer DynamoDB pour le stockage de données serverless
  • Utiliser le développement Lambda en direct pour un feedback instantané
  • Configurer des domaines personnalisés et des stages de production
  • Gérer plusieurs environnements (dev, staging, production)

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé
  • AWS CLI configuré avec vos identifiants (aws configure)
  • Un compte AWS avec des permissions administrateur (ou au minimum IAM, S3, CloudFront, Lambda, DynamoDB)
  • Des connaissances de base en Next.js App Router et TypeScript
  • Un éditeur de code (VS Code recommandé)

SST déploie de vraies ressources AWS qui peuvent engendrer des coûts. Les ressources de ce tutoriel restent dans le Free Tier AWS dans la plupart des cas, mais surveillez toujours votre tableau de bord de facturation.

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez :

  • Une application Next.js déployée sur AWS via CloudFront (CDN mondial)
  • Un téléchargement de fichiers via S3 avec des URLs présignées
  • Une table DynamoDB pour stocker des données
  • Un environnement de développement Lambda en direct avec rechargement instantané
  • Des environnements staging et production séparés
  • Un domaine personnalisé avec SSL automatique

Étape 1 : Créer le projet Next.js

Commencez par créer une nouvelle application Next.js :

npx create-next-app@latest sst-nextjs-app
cd sst-nextjs-app

Sélectionnez les options suivantes :

  • TypeScript : Oui
  • ESLint : Oui
  • Tailwind CSS : Oui
  • Répertoire src/ : Oui
  • App Router : Oui
  • Alias d'import : @/*

Étape 2 : Initialiser SST

Installez SST dans le projet :

npx sst@latest init

Quand on vous le demande, sélectionnez aws comme fournisseur. Cela génère un fichier sst.config.ts à la racine du projet :

/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
  app(input) {
    return {
      name: "sst-nextjs-app",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    };
  },
  async run() {
    new sst.aws.Nextjs("MyWeb");
  },
});

Décortiquons ce qui se passe ici :

  • app() configure le nom de l'application, la politique de suppression et le fournisseur principal
  • removal: "retain" conserve les ressources lors de la suppression d'un stage de production (filet de sécurité)
  • protect empêche la suppression accidentelle des ressources de production
  • async run() définit votre infrastructure — actuellement juste un site Next.js
  • sst.aws.Nextjs déploie votre application via OpenNext (CloudFront + S3 + Lambda)

SST crée également un répertoire .sst/ pour les types et l'état. Ajoutez-le à .gitignore :

echo ".sst" >> .gitignore

Étape 3 : Déployer sur AWS (Premier déploiement)

Déployez l'application sur un stage de développement :

npx sst deploy --stage dev

Le premier déploiement prend 3 à 5 minutes pour provisionner CloudFront, S3 et Lambda. Les déploiements suivants sont beaucoup plus rapides. Une fois terminé, SST affiche votre URL CloudFront :

✓  Complete
   MyWeb: https://d1234abcd.cloudfront.net

Ouvrez l'URL — votre application Next.js est en ligne sur AWS.

Chaque stage obtient son propre ensemble isolé de ressources AWS. Vous pouvez créer autant de stages que nécessaire : dev, staging, production, pr-123, etc.

Étape 4 : Ajouter le téléchargement de fichiers S3

Ajoutons un bucket S3 pour le téléchargement de fichiers et lions-le à l'application Next.js.

4.1 Mettre à jour la configuration SST

Modifiez sst.config.ts pour ajouter un bucket S3 public :

/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
  app(input) {
    return {
      name: "sst-nextjs-app",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    };
  },
  async run() {
    const bucket = new sst.aws.Bucket("Uploads", {
      access: "public",
    });
 
    new sst.aws.Nextjs("MyWeb", {
      link: [bucket],
    });
 
    return {
      bucket: bucket.name,
    };
  },
});

La propriété link est la magie de SST. Elle accorde à vos fonctions serveur Next.js les permissions IAM sur le bucket et injecte le nom du bucket comme variable d'environnement — pas de politiques IAM manuelles ni de fichiers .env à gérer.

4.2 Installer le SDK AWS et le SDK SST

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner sst

4.3 Créer la route API de téléchargement

Créez une Server Action qui génère une URL présignée pour le téléchargement direct vers S3 :

// src/app/api/upload/route.ts
import { Resource } from "sst";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { NextResponse } from "next/server";
 
const s3 = new S3Client({});
 
export async function POST(request: Request) {
  const { filename, contentType } = await request.json();
 
  const key = `uploads/${Date.now()}-${filename}`;
 
  const command = new PutObjectCommand({
    Bucket: Resource.Uploads.name,
    Key: key,
    ContentType: contentType,
  });
 
  const presignedUrl = await getSignedUrl(s3, command, {
    expiresIn: 3600,
  });
 
  return NextResponse.json({
    presignedUrl,
    key,
    publicUrl: `https://${Resource.Uploads.name}.s3.amazonaws.com/${key}`,
  });
}

Remarquez Resource.Uploads.name — SST injecte automatiquement le nom du bucket lié. Pas de valeurs codées en dur, pas de variables d'environnement à gérer.

4.4 Créer l'interface de téléchargement

// src/app/upload/page.tsx
"use client";
 
import { useState } from "react";
 
export default function UploadPage() {
  const [file, setFile] = useState<File | null>(null);
  const [uploading, setUploading] = useState(false);
  const [uploadedUrl, setUploadedUrl] = useState<string | null>(null);
 
  async function handleUpload() {
    if (!file) return;
    setUploading(true);
 
    try {
      const res = await fetch("/api/upload", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          filename: file.name,
          contentType: file.type,
        }),
      });
      const { presignedUrl, publicUrl } = await res.json();
 
      await fetch(presignedUrl, {
        method: "PUT",
        headers: { "Content-Type": file.type },
        body: file,
      });
 
      setUploadedUrl(publicUrl);
    } catch (error) {
      console.error("Échec du téléchargement:", error);
    } finally {
      setUploading(false);
    }
  }
 
  return (
    <div className="max-w-md mx-auto p-8">
      <h1 className="text-2xl font-bold mb-4">Téléchargement de fichiers</h1>
      <input
        type="file"
        onChange={(e) => setFile(e.target.files?.[0] || null)}
        className="mb-4 block w-full"
      />
      <button
        onClick={handleUpload}
        disabled={!file || uploading}
        className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
      >
        {uploading ? "Téléchargement..." : "Envoyer vers S3"}
      </button>
 
      {uploadedUrl && (
        <div className="mt-4 p-4 bg-green-50 rounded">
          <p className="text-sm text-green-800">Téléchargement réussi !</p>
          <a
            href={uploadedUrl}
            target="_blank"
            rel="noopener noreferrer"
            className="text-blue-600 underline text-sm break-all"
          >
            {uploadedUrl}
          </a>
        </div>
      )}
    </div>
  );
}

Étape 5 : Ajouter DynamoDB pour le stockage de données

5.1 Définir la table dans la configuration SST

Mettez à jour sst.config.ts pour ajouter une table DynamoDB :

/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
  app(input) {
    return {
      name: "sst-nextjs-app",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    };
  },
  async run() {
    const bucket = new sst.aws.Bucket("Uploads", {
      access: "public",
    });
 
    const table = new sst.aws.Dynamo("Notes", {
      fields: {
        userId: "string",
        noteId: "string",
      },
      primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
    });
 
    new sst.aws.Nextjs("MyWeb", {
      link: [bucket, table],
    });
 
    return {
      bucket: bucket.name,
      table: table.name,
    };
  },
});

5.2 Installer le client DynamoDB

npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

5.3 Créer l'API des notes

// src/app/api/notes/route.ts
import { Resource } from "sst";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  PutCommand,
  QueryCommand,
} from "@aws-sdk/lib-dynamodb";
import { NextResponse } from "next/server";
 
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const userId = searchParams.get("userId") || "anonymous";
 
  const result = await client.send(
    new QueryCommand({
      TableName: Resource.Notes.name,
      KeyConditionExpression: "userId = :userId",
      ExpressionAttributeValues: { ":userId": userId },
    })
  );
 
  return NextResponse.json({ notes: result.Items || [] });
}
 
export async function POST(request: Request) {
  const { userId = "anonymous", title, content } = await request.json();
 
  const note = {
    userId,
    noteId: `note_${Date.now()}`,
    title,
    content,
    createdAt: new Date().toISOString(),
  };
 
  await client.send(
    new PutCommand({
      TableName: Resource.Notes.name,
      Item: note,
    })
  );
 
  return NextResponse.json({ note });
}

5.4 Créer la page des notes

// src/app/notes/page.tsx
"use client";
 
import { useEffect, useState } from "react";
 
interface Note {
  noteId: string;
  title: string;
  content: string;
  createdAt: string;
}
 
export default function NotesPage() {
  const [notes, setNotes] = useState<Note[]>([]);
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
 
  async function loadNotes() {
    const res = await fetch("/api/notes?userId=anonymous");
    const data = await res.json();
    setNotes(data.notes);
  }
 
  async function createNote() {
    if (!title || !content) return;
 
    await fetch("/api/notes", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title, content }),
    });
 
    setTitle("");
    setContent("");
    loadNotes();
  }
 
  useEffect(() => {
    loadNotes();
  }, []);
 
  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-2xl font-bold mb-6">Notes Serverless</h1>
 
      <div className="mb-8 space-y-3">
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="Titre de la note"
          className="w-full border rounded px-3 py-2"
        />
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="Contenu de la note"
          className="w-full border rounded px-3 py-2 h-24"
        />
        <button
          onClick={createNote}
          className="bg-blue-600 text-white px-4 py-2 rounded"
        >
          Ajouter une note
        </button>
      </div>
 
      <div className="space-y-4">
        {notes.map((note) => (
          <div key={note.noteId} className="border rounded p-4">
            <h2 className="font-semibold">{note.title}</h2>
            <p className="text-gray-600 mt-1">{note.content}</p>
            <p className="text-xs text-gray-400 mt-2">{note.createdAt}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Étape 6 : Développement Lambda en direct

C'est ici que SST brille vraiment. Au lieu de redéployer à chaque modification du code serveur, sst dev redirige les invocations Lambda vers votre machine locale en temps réel.

Lancez l'environnement de développement :

npx sst dev

SST fait trois choses simultanément :

  1. Déploie l'infrastructure sur AWS (S3, DynamoDB, CloudFront)
  2. Redirige les requêtes Lambda vers votre machine locale (rechargement instantané)
  3. Lance le serveur de développement Next.js sur localhost:3000

Modifiez une route API, sauvegardez le fichier et rafraîchissez — le changement est immédiat. Pas de redéploiement, pas d'attente. C'est possible car SST remplace la fonction Lambda par un stub qui transfère les invocations vers votre machine via une connexion WebSocket.

Le développement Lambda en direct nécessite que vos identifiants AWS soient configurés. SST crée une connexion WebSocket légère via IoT pour le proxy — votre code s'exécute localement mais accède aux vraies ressources AWS.

Étape 7 : Configuration du domaine personnalisé

7.1 Avec Route 53

Si votre domaine est géré par Route 53, ajoutez l'option domain au composant Nextjs :

new sst.aws.Nextjs("MyWeb", {
  link: [bucket, table],
  domain: {
    name: "app.votredomaine.com",
    dns: sst.aws.dns(),
  },
});

SST crée automatiquement les enregistrements Route 53 et provisionne un certificat SSL via ACM.

7.2 Avec Cloudflare DNS

Pour les domaines gérés par Cloudflare :

new sst.aws.Nextjs("MyWeb", {
  link: [bucket, table],
  domain: {
    name: "app.votredomaine.com",
    dns: sst.cloudflare.dns(),
  },
});

7.3 Avec un DNS externe

Pour d'autres fournisseurs, SST affiche les enregistrements DNS requis et attend que vous les configuriez :

new sst.aws.Nextjs("MyWeb", {
  link: [bucket, table],
  domain: {
    name: "app.votredomaine.com",
    dns: false,
  },
});

Étape 8 : Configuration par environnement

Les stages SST vous permettent de créer des environnements isolés. Utilisez la variable $app.stage pour configurer les ressources différemment selon le stage :

async run() {
  const isProd = $app.stage === "production";
 
  const bucket = new sst.aws.Bucket("Uploads", {
    access: "public",
  });
 
  const table = new sst.aws.Dynamo("Notes", {
    fields: {
      userId: "string",
      noteId: "string",
    },
    primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
    pointInTimeRecovery: isProd,
  });
 
  new sst.aws.Nextjs("MyWeb", {
    link: [bucket, table],
    domain: isProd
      ? { name: "app.votredomaine.com", dns: sst.aws.dns() }
      : undefined,
    server: {
      memory: isProd ? "1024 MB" : "512 MB",
    },
  });
}

Déployez sur différents stages :

# Développement
npx sst deploy --stage dev
 
# Staging
npx sst deploy --stage staging
 
# Production
npx sst deploy --stage production

Chaque stage crée des ressources complètement isolées — des buckets S3 séparés, des tables DynamoDB, des distributions CloudFront et des fonctions Lambda.

Étape 9 : Ajouter une tâche planifiée

SST facilite l'ajout de tâches planifiées. Ajoutons un cron job qui nettoie les anciens fichiers :

// Ajoutez dans sst.config.ts à l'intérieur de async run()
new sst.aws.Cron("CleanupOldUploads", {
  schedule: "rate(1 day)",
  job: {
    handler: "src/functions/cleanup.handler",
    link: [bucket],
  },
});

Créez le handler :

// src/functions/cleanup.ts
import { Resource } from "sst";
import {
  S3Client,
  ListObjectsV2Command,
  DeleteObjectsCommand,
} from "@aws-sdk/client-s3";
 
const s3 = new S3Client({});
const DAYS_TO_KEEP = 30;
 
export async function handler() {
  const cutoff = new Date();
  cutoff.setDate(cutoff.getDate() - DAYS_TO_KEEP);
 
  const objects = await s3.send(
    new ListObjectsV2Command({
      Bucket: Resource.Uploads.name,
      Prefix: "uploads/",
    })
  );
 
  const toDelete = (objects.Contents || [])
    .filter((obj) => obj.LastModified && obj.LastModified < cutoff)
    .map((obj) => ({ Key: obj.Key! }));
 
  if (toDelete.length > 0) {
    await s3.send(
      new DeleteObjectsCommand({
        Bucket: Resource.Uploads.name,
        Delete: { Objects: toDelete },
      })
    );
    console.log(`${toDelete.length} anciens fichiers supprimés`);
  }
 
  return { deleted: toDelete.length };
}

Étape 10 : Déploiement en production

10.1 Configuration SST finale

Voici le fichier sst.config.ts complet avec toutes les ressources :

/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
  app(input) {
    return {
      name: "sst-nextjs-app",
      removal: input?.stage === "production" ? "retain" : "remove",
      protect: ["production"].includes(input?.stage),
      home: "aws",
    };
  },
  async run() {
    const isProd = $app.stage === "production";
 
    const bucket = new sst.aws.Bucket("Uploads", {
      access: "public",
    });
 
    const table = new sst.aws.Dynamo("Notes", {
      fields: {
        userId: "string",
        noteId: "string",
      },
      primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
      pointInTimeRecovery: isProd,
    });
 
    new sst.aws.Nextjs("MyWeb", {
      link: [bucket, table],
      domain: isProd
        ? { name: "app.votredomaine.com", dns: sst.aws.dns() }
        : undefined,
      server: {
        memory: isProd ? "1024 MB" : "512 MB",
        architecture: "arm64",
      },
    });
 
    new sst.aws.Cron("CleanupOldUploads", {
      schedule: "rate(1 day)",
      job: {
        handler: "src/functions/cleanup.handler",
        link: [bucket],
      },
    });
 
    return {
      bucket: bucket.name,
      table: table.name,
    };
  },
});

10.2 Déployer en production

npx sst deploy --stage production

10.3 Surveiller votre déploiement

SST affiche tous les détails des ressources après le déploiement. Vous pouvez aussi les consulter dans la console AWS :

  • CloudFront — votre distribution CDN et domaine
  • S3 — votre bucket de téléchargement et assets statiques
  • Lambda — vos fonctions serveur
  • DynamoDB — votre table de données

10.4 Supprimer un stage

Pour détruire un stage non-production et supprimer toutes ses ressources :

npx sst remove --stage dev

Les stages de production avec protect: true ne peuvent pas être supprimés accidentellement. Vous devez d'abord définir protect à false dans la configuration avant de supprimer.

Dépannage

"Cannot find module 'sst'" dans Next.js

Assurez-vous d'avoir installé le package sst :

npm install sst

Le premier déploiement est lent

Le déploiement initial provisionne une distribution CloudFront, ce qui prend 3 à 5 minutes. Les déploiements suivants sont beaucoup plus rapides (généralement moins de 60 secondes).

Erreurs "Access Denied"

Vérifiez que vos identifiants AWS ont des permissions suffisantes. SST nécessite l'accès à IAM, S3, CloudFront, Lambda, DynamoDB, CloudWatch et SSM au minimum. L'utilisation d'un rôle administrateur pour le développement est recommandée.

Le dev en direct ne se connecte pas

Vérifiez que vos identifiants AWS sont valides et que le port 443 sortant n'est pas bloqué par votre pare-feu. SST utilise AWS IoT Core pour la connexion WebSocket.

SST vs autres options de déploiement

FonctionnalitéSST IonVercelAWS CDKTerraform
LangageTypeScriptN/A (UI)TypeScriptHCL
Dev en directOui (sub-seconde)NonNonNon
Liaison de ressourcesAutomatiqueVariables env manuellesManuelManuel
CoûtPrix AWS uniquementPar siège + usagePrix AWSPrix AWS
Support Next.jsComplet (OpenNext)NatifManuelManuel
Multi-fournisseurPlus de 150Vercel uniquementAWS uniquementPlus de 150

Prochaines étapes

Maintenant que vous avez une application Next.js full-stack sur AWS, envisagez :

  • Ajouter l'authentification avec sst.aws.Cognito ou intégrer NextAuth.js
  • Mettre en place un pipeline CI/CD avec GitHub Actions exécutant npx sst deploy --stage production
  • Ajouter une API Gateway avec sst.aws.ApiGatewayV2 pour des APIs autonomes
  • Explorer la console SST sur console.sst.dev pour un tableau de bord visuel de vos ressources
  • Ajouter une file d'attente avec sst.aws.Queue pour le traitement en arrière-plan

Conclusion

SST Ion transforme le déploiement AWS d'un casse-tête DevOps en une expérience conviviale pour les développeurs. En définissant l'infrastructure en TypeScript aux côtés de votre code applicatif, vous obtenez la sécurité des types, la liaison de ressources et le développement en direct — le tout en déployant sur votre propre compte AWS sans surcoût.

Les points clés :

  • Infrastructure as Code en TypeScript — pas de YAML, pas de fichiers de configuration séparés
  • La liaison de ressources élimine les politiques IAM manuelles et les variables d'environnement
  • Le développement Lambda en direct fournit un feedback instantané sans redéploiement
  • Les stages vous donnent des environnements isolés gratuitement
  • OpenNext déploie Next.js sur AWS avec un support complet des fonctionnalités

SST vous permet de posséder votre infrastructure sans la complexité traditionnelle d'AWS. Commencez avec npx sst@latest init et déployez votre première application en quelques minutes.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Comment vérifier si votre application est affectée par l'attaque supply chain Axios (et la corriger).

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes