Déployer une Application Next.js sur Cloud Run

  • posté le
  • par ESENS

Chez ESENS, l'exploration de différentes technologies est au cœur de nos projets, que ce soit pour nos clients ou dans le cadre de développements internes. Pour favoriser la montée en compétences de nos équipes, nous avons mis en place le projet "Pizza Shop". Cette application permet à chaque collaborateur d'approfondir ses connaissances sur les dernières versions des langages, outils et frameworks de son choix.

Dans cet article, nous partageons notre expérience sur le déploiement d'une version du Pizza Shop, cette fois-ci développée avec Next.js, sur le service Cloud Run de la plateforme Google Cloud.

Avant d’être développée avec Next.js, une première version du Pizza Shop a également été développée avec React.js, ce qui nous permet de partager un retour d’expérience sur le passage de React.js à Next.js à la fin de cet article.

Bonne lecture !

Application Next.js

Next.js est un framework JavaScript open-source basé sur React.js et permettant le développement d'applications web modernes et performantes. 

Contrairement à React.js, qui est une librairie dédiée uniquement au développement Front-End, Next.js est un framework dit “full-stack” car il permet à la fois de développer la partie Front-End et la partie Back-End d’un projet web.

Cela lui permet de proposer des fonctionnalités avancées telles que le pré-rendu côté client (CSR), le rendu côté serveur (SSR), la génération de site statique (SSG) et la régénération statique incrémentielle (ISR).

Chacune de vos pages web peuvent utiliser l’une de ces fonctionnalités indépendamment des autres pages afin d’avoir un site web entièrement optimisé selon ses besoins. Next.js peut également être couplé à la syntaxe TypeScript, au framework CSS Tailwind, à l’outil d'analyse de code ESLint, et peut fournir de base certains modules indispensables comme un routeur innovant et la solution de tests unitaire automatisés Jest. Toutes ces options vous sont proposées dès la création du projet pour une intégration facile, rapide et prête à l’emploi.

Les étapes décrites ci-dessous sont basées sur un environnement Linux Ubuntu. Pour d'autres systèmes d'exploitation, les commandes peuvent être adaptées en suivant les liens associés.

Création du Projet Next.js

Comme Next.js utilise Node.js, assurez-vous d'avoir Node.js et npm installés sur votre machine.

Ensuite, initialisez un projet Next.js avec les commandes suivantes :

npx create-next-app pizza-shop

Des questions vous sont posées pour activer automatiquement certains modules (Typescript, EsLint, Tailwind, Routeur, etc.)

Une fois le projet créé, entrez dans le dossier qui a été généré.

cd pizza-shop

Note: Vous pouvez remplacer “pizza shop” pour le nom de votre propre projet.

Vous pouvez maintenant lancer l'application en utilisant :

npm run dev

L'application Next.js est accessible localement à l’adresse http://localhost:3000

Déploiement sur Cloud Run

Chez ESENS, nous privilégions le déploiement sur Cloud Run, un service serverless de type Container as a Service (CAAS) proposé par Google Cloud Platform.

Construction de l’Image Docker

La première étape consiste à créer une image Docker pour l'application Next.js. Créez un fichier Dockerfile à la racine du projet avec le contenu suivant :

# Première étape - Installation des dépendances Node.js
FROM node:alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
# Deuxième étape - Construction de l'application Next.js
COPY . .
RUN npm run build
# Troisième étape - Démarrage de l'application
EXPOSE 3000
CMD ["npm", "start"]

Déploiement sur Cloud Run

Pour déployer l'application Next.js sur Cloud Run depuis votre poste de travail, suivez ces étapes :

  1. Créez un projet Google Cloud.
  2. Assurez-vous que la facturation est activée pour votre projet Google Cloud.
  3. Installez le Google Cloud SDK sur votre poste de travail.

Ensuite, initialisez le Google Cloud CLI :

gcloud init

Définissez le projet que vous avez créé comme le projet par défaut pour Cloud Run :

gcloud auth application-default login
gcloud config set project PROJECT_ID

Remplacez PROJECT_ID par votre identifiant de projet Google Cloud.

Maintenant vous pouvez déployer votre service directement à partir du code source avec la commande suivante:

gcloud run deploy pizza-shop --source . --allow-unauthenticated --zone europe-west3

Lors du déploiement, acceptez d'activer les API nécessaires pour le bon fonctionnement du service.

Une fois le déploiement terminé, vous disposerez d'une URL publique pour accéder à votre application Next.js ou l’on trouve notre application Pizza-Shop.

Ici vous pouvez trouver plus d’informations sur les déploiements sur Cloud Run.

Industrialisation

Bien que la méthode décrite soit adaptée pour un déploiement simple depuis votre poste, dans un contexte d'industrialisation, la création d'une chaîne CI/CD est essentielle.

Chez ESENS, nous avons mis en place une chaîne pour déployer nos projets sur Cloud Run. Cette chaîne utilise des workflows GitHub Actions pour activer les API nécessaires, construire l'image Docker, la pousser sur Google Container Registry (GCR), puis la déployer sur Cloud Run. Le tout, en communiquant avec GCP en utilisant Workload Identity.

Passer de React.js à Next.js

Passer de React.js à Next.js est une transition relativement fluide pour la plupart des projets, car Next.js est construit au-dessus de React.js et facilite le développement des applications React.js en ajoutant des fonctionnalités telles que le rendu côté serveur, le routage côté serveur, le préchargement des pages, etc.

Cependant, il peut y avoir quelques différences et défis à prendre en compte. Voici quelques points à considérer basés sur mon retour d’expérience :

1. Structure du Projet :

Next.js a une structure de projet différente de celle d'une application React.js classique. Vous devrez vous familiariser avec le dossier pages de Next.js, où chaque fichier représente une route.

La structure du dossier pages de Next.js est cruciale. Voici un exemple simple avec deux pages:

/pages
  /index.js
  /about.js

Le fichier index.js représente la page d'accueil, et about.js représente la page "À propos".

En React, vous pouvez organiser votre projet de manière similaire, mais sans la structure spécifique de Next.js.

Par exemple:

/src
  /components
    /Home.js
    /About.js

Le fichier Home.js représente la page d'accueil, et About.js représente la page "À propos".

2. Routage :

Next.js gère le routage côté serveur, ce qui peut différer de la gestion côté client de React.js. Certaines logiques de routage ou de navigation ont dû nécessiter des ajustements.

Dans Next.js, le routage est géré par les fichiers du dossier "pages". Voici un exemple de lien entre deux pages:

// Dans index.js
import Link from 'next/link';
function HomePage() {
  return (
<div>
      <h1>Accueil</h1>
      <Link href="/about">
        <a>À propos de nous</a>
      </Link>
</div>
  );
}

En React, vous pouvez utiliser une bibliothèque de routage comme react-router-dom. Voici un exemple:

// Dans Home.js
import React from 'react';
import { Link } from 'react-router-dom';
function Home() {
  return (
    <div>
      <h1>Accueil</h1>
      <Link to="/about">À propos de nous</Link>
    </div>
  );
}

3. Server-side Rendering (SSR) :

Avec Next.js, vous avez la possibilité d'utiliser le rendu côté serveur (SSR). Cela a nécessité des ajustements dans la gestion de l'état global, car le code côté client s'exécute après le rendu initial côté serveur.

Utiliser le rendu côté serveur peut nécessiter des ajustements dans la gestion de l'état global. 

Exemple avec getServerSideProps:

// Dans une page avec SSR (par exemple, pages/about.js)
function AboutPage({ data }) {
  return (
    <div>
      <h1>À propos de nous</h1>
      <p>{data.description}</p>
    </div>
  );
}
export async function getServerSideProps() {
  // Appel à une API ou chargement de données côté serveur
  const data = await fetchData();
  return {
    props: {
      data,
    },
  };
}

En React, le rendu côté serveur est souvent géré avec des solutions comme ReactDOMServer.

Voici un exemple simplifié:

// Dans About.js
import React from 'react';
import { fetchData } from '../utils/api';
class About extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
    };
  }
  async componentDidMount() {
    try {
      const data = await fetchData();
      this.setState({ data });
    } catch (error) {
      console.error('Erreur lors de la récupération des données', error);
    }
  }
  render() {
    return (
      <div>
        <h1>À propos de nous</h1>
        <p>{this.state.data ? this.state.data : 'Chargement des données...'}</p>
      </div>
    );
  }
}

4. Modules équivalents :

La plupart des modules React.js sont compatibles avec Next.js, mais vous devrez peut-être faire quelques ajustements, en particulier si vous migrez vers des fonctionnalités spécifiques à Next.js comme le getServerSideProps ou getStaticProps pour le rendu des pages.

Exemple avec getStaticProps:

// Dans une page avec rendu statique (par exemple, pages/index.js)
function Home({ data }) {
  return (
    <div>
      <h1>Bienvenue {data.user}  !</h1>
    </div>
  );
}
export async function getStaticProps() {
  // Appel à une API ou chargement de données pour le rendu statique
  const data = await fetchData();
  return {
    props: {
      data,
    },
  };
}

En React, l'utilisation de modules est similaire. Voici un exemple:

// Dans Home.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../utils/api';
function Home() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  useEffect(() => {
const fetchDataFromAPI = async () => {
      try {
        const response = await fetchData();
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
};
fetchDataFromAPI();
  }, []); // Le tableau vide en tant que deuxième argument signifie que useEffect ne s'exécute qu'une seule fois (équivalent de componentDidMount)
  return (
<div>
      <h1>Accueil</h1>
      {loading && <p>Chargement des données...</p>}
      {error && <p>Erreur lors de la récupération des données: {error.message}</p>}
      {data && <p>Données récupérées: {data}</p>}
</div>
  );
}
export default Home;

Dans cet exemple:

- useState est utilisé pour gérer l'état du composant.

- useEffect est utilisé pour déclencher l'appel à l'API au montage du composant.

- L'utilisation de fetchData (supposée être une fonction qui retourne une promesse) dans la fonction fetchDataFromAPI est illustrée.

5. Tests Unitaires :

Les tests unitaires peuvent nécessiter des modifications en raison de l'introduction de nouvelles fonctionnalités liées à SSR ou de la structure du projet. Les bibliothèques de test comme Jest peuvent toujours être utilisées, mais certaines configurations ont dû nécessiter des ajustements.

// Exemple de test Jest pour une fonction React
import { render, screen } from '@testing-library/react';
import HomePage from '../pages/index';
test('Rend la page d\'accueil avec un titre', () => {
  render(<HomePage />);
  const titleElement = screen.getByText(/Accueil/i);
  expect(titleElement).toBeInTheDocument();
});

Voici un exemple d'utilisation de mocks dans les tests unitaires avec Jest, en particulier pour simuler un appel à une API dans un composant React. Dans cet exemple, nous allons mocker une fonction qui effectue une requête HTTP pour récupérer des données.

Supposons que nous avons une fonction fetchData qui effectue une requête HTTP pour récupérer des données dans notre composant React. Nous allons mocker cette fonction dans le test unitaire pour éviter les appels réseau réels.

// Composant React avec une fonction fetchData
// Exemple: components/ExampleComponent.js
import React, { useState, useEffect } from 'react';
import fetchData from '../utils/api'; // La fonction à mocker
function ExampleComponent() {
  const [data, setData] = useState(null);
  useEffect(() => {
    const fetchDataFromAPI = async () => {
      try {
        const result = await fetchData();
        setData(result);
      } catch (error) {
        console.error('Erreur lors de la récupération des données', error);
      }
    };
    fetchDataFromAPI();
  }, []);
  return (
    <div>
      {data ? (
        <p>Données récupérées: {data}</p>
      ) : (
        <p>Chargement des données...</p>
      )}
    </div>
  );
}
export default ExampleComponent;

Maintenant, le test unitaire avec Jest et l'utilisation de mocks:

// Test unitaire avec Jest et utilisation de mocks
// Exemple: __tests__/ExampleComponent.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ExampleComponent from '../components/ExampleComponent';
import fetchData from '../utils/api';
// Mock de la fonction fetchData
jest.mock('../utils/api');
test('Rend le composant avec les données récupérées', async () => {
  // Définir le comportement du mock
  fetchData.mockResolvedValue('Données simulées');
  // Rend le composant
  render(<ExampleComponent />);
  // Vérifie que le texte de chargement des données est affiché
  const loadingText = screen.getByText(/Chargement des données.../i);
  expect(loadingText).toBeInTheDocument();
  // Attend que les données soient récupérées et vérifie leur affichage
  const dataText = await screen.findByText(/Données récupérées: Données simulées/i);
  expect(dataText).toBeInTheDocument();
});
test('Gère les erreurs lors de la récupération des données', async () => {
  // Définir le comportement du mock pour simuler une erreur
  fetchData.mockRejectedValue(new Error('Erreur simulée'));
  // Rend le composant
  render(<ExampleComponent />);
  // Vérifie que le texte d'erreur est affiché
  const errorText = await screen.findByText(/Erreur lors de la récupération des données/i);
  expect(errorText).toBeInTheDocument();
});

Dans cet exemple, le mock de fetchData est créé avec jest.mock et son comportement est configuré avec mockResolvedValue et mockRejectedValue pour simuler le succès et l'échec de la requête. Ces mocks garantissent que le test unitaire n'effectue pas de véritables appels réseau.

6. Middleware et API Routes :

Next.js propose des API Routes qui peuvent simplifier la gestion des points de terminaison côté serveur. Nous avons dû migrer des middleware Express et des routes API.

Exemple de création d'une API Route:

// Dans pages/api/example.js
export default function handler(req, res) {
  // Logique de l'API
  res.status(200).json({ message: 'Exemple d\'API Next.js' });
}

En React, la gestion des points de terminaison côté serveur peut être réalisée avec des middleware Express, par exemple.

Voici un exemple simplifié:

// Exemple de middleware Express dans votre serveur Node.js
const express = require('express');
const app = express();
app.get('/api/example', (req, res) => {
  // Logique de l'API
  res.json({ message: 'Exemple d\'API React' });
});
// Autres configurations et écoute du serveur

7. Optimisation des Performances :

Next.js offre des fonctionnalités telles que le prérendu statique et le préchargement des pages. Vous pouvez revoir et ajuster vos stratégies d'optimisation des performances.

Utilisation de la prélecture de pages avec next/link:

// Dans une page avec utilisation de prélecture (par exemple, pages/index.js)
import Link from 'next/link';
function HomePage() {
  return (
    <div>
      <h1>Accueil</h1>
      <Link href="/about" prefetch>
        <a>À propos de nous</a>
      </Link>
    </div>
  );
}

En React, l'optimisation des performances peut être réalisée de différentes manières, telles que le chargement dit “paresseux” (lazy loading)  des composants. Voici un exemple simplifié:

// Dans Home.js avec React.lazy pour le chargement paresseux
import React, { lazy, Suspense } from 'react';
const About = lazy(() => import('./About'));
function Home() {
  return (
    <div>
      <h1>Accueil</h1>
      <Suspense fallback={<div>Chargement...</div>}>
        <About />
      </Suspense>
    </div>
  );
}

Conclusion

En résumé, bien que la migration de React.js à Next.js soit généralement lisse, la prudence et la compréhension approfondie des fonctionnalités spécifiques de Next.js sont essentielles pour minimiser les difficultés. Les ajustements nécessaires dépendent de la complexité de votre application et de la manière dont vous avez mis en œuvre React.js.

Voilà, nous espérons que cet article vous a été utile et vous inspire pour déployer vos applications Next.js sur Cloud Run !


------------------------------

Article rédigé par Florian, Développeur Front-End, et Andrés, Tech Lead Cloud & IoT et membre de la DT ESENS.

Retrouvez tous nos articles tech sur le Blog ESENS !

Vous êtes à la recherche d’un nouveau challenge technique ? Découvrez la dream team et rejoignez-nous en postulant à nos offres d’emploi!


PARTAGER CET ARTICLE