window.pipedriveLeadboosterConfig = { base : 'leadbooster-chat.pipedrive.com', companyId : 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version : 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster existe déjà') } else { w.LeadBooster = { q : [], on : function (n, h) { this.q.push({ t : 'o', n : n, h : h }) }, trigger : function (n) { this.q.push({ t : 't', n : n }) }, } } })() Déployer une API GraphQL/MongoDB avec Netlify Functions - The Codest
The Codest
  • A propos de nous
  • Services
    • Développement de logiciels
      • Développement frontal
      • Développement backend
    • Staff Augmentation
      • Développeurs frontaux
      • Développeurs backend
      • Ingénieurs des données
      • Ingénieurs en informatique dématérialisée
      • Ingénieurs AQ
      • Autres
    • Conseil consultatif
      • Audit et conseil
  • Industries
    • Fintech et banque
    • E-commerce
    • Adtech
    • Santé (Healthtech)
    • Fabrication
    • Logistique
    • Automobile
    • IOT
  • Valeur pour
    • CEO
    • CTO
    • Gestionnaire des livraisons
  • Notre équipe
  • Études de cas
  • Savoir comment
    • Blog
    • Rencontres
    • Webinaires
    • Ressources
Carrières Prendre contact
  • A propos de nous
  • Services
    • Développement de logiciels
      • Développement frontal
      • Développement backend
    • Staff Augmentation
      • Développeurs frontaux
      • Développeurs backend
      • Ingénieurs des données
      • Ingénieurs en informatique dématérialisée
      • Ingénieurs AQ
      • Autres
    • Conseil consultatif
      • Audit et conseil
  • Valeur pour
    • CEO
    • CTO
    • Gestionnaire des livraisons
  • Notre équipe
  • Études de cas
  • Savoir comment
    • Blog
    • Rencontres
    • Webinaires
    • Ressources
Carrières Prendre contact
Flèche arrière RETOUR
2021-05-13
Développement de logiciels

Déployer une API GraphQL/MongoDB à l'aide des fonctions Netlify

The Codest

Pawel Rybczynski

Software Engineer

Objectifs Configuration initiale Installation des dépendances Commençons Tout d'abord, ajoutez le fichier tsconfig.json dans le répertoire principal : Maintenant, créons src/server.ts pour les implémentations des serveurs. Puis ajoutons deux fonctions : l'une pour le serveur local et l'autre pour le lambda. OK, nous n'avons pas de résolveurs ou de définitions de types, nous devons donc en créer. Supposons que, dans un premier temps, nous voulions [...]

Objectifs

  1. Configurer les serveurs locaux et les serveurs lambda.
  2. Connectez les deux à MongoDB.
  3. Mettre en œuvre l'authentification de base.
  4. Déployer Apollo sans serveur GraphQL API avec Netlify.
  5. Utiliser Typescript.

Configuration initiale

npm init -y

Installer les dépendances

npm install --save typescript graphql aws-lambda @types/aws-lambda

Commençons

Tout d'abord, ajoutez le tsconfig.json dans le répertoire principal :

 {
 "compilerOptions" : {
 "target" : "es5",
 "module" : "commonjs",
 "allowJs" : true,
 "strict" : true,
 "esModuleInterop" : true,
 "skipLibCheck" : true,
 "forceConsistentCasingInFileNames" : true
 },
 "include" : ["src/*.ts", "src/**/*.ts", "src/**/*.js"],
 "exclude" : ["node_modules"]
 }

Maintenant, créons src/server.ts pour les implémentations de serveurs. Ajoutez ensuite deux fonctions : l'une pour le serveur local et l'autre pour le serveur lambda.

// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda" ;
import { ApolloServer } from "apollo-server" ;


const createLambdaServer = () =>
  nouveau ApolloServerLambda({
    typeDefs,
    résolveurs,
    introspection : true,
    terrain de jeu : true,
    },
  }) ;

const createLocalServer = () =>
  nouveau ApolloServer({
    typeDefs,
    résolveurs,
    introspection : true,
    terrain de jeu : true,
    },
  }) ;

export { createLambdaServer, createLocalServer } ;

OK, nous n'avons pas de résolveurs ou de définitions de types, nous devons donc en créer. Supposons que, dans un premier temps, nous voulions créer des utilisateurs et recevoir des informations à leur sujet.

// src/schemas.ts
const { gql } = require("apollo-server-lambda") ;

const userSchema = gql`
  type User {
    id : ID !
    email : Chaîne !
    name : Chaîne !
  }

  type Query {
    user(id : ID !): Utilisateur !
  }

  type Mutation {
    createUser(name : String !, email : String !, password : String !): Utilisateur !
  }
` ;

Si vous n'êtes pas familier avec cela, Apollo a préparé un très beau tutoriel

Créons maintenant un résolveur d'utilisateurs avec une requête et une mutation.

// src/resolvers.ts
const userResolver = {
  Query : {
    user : async (parent, args, context, info) => {
      {...}
    },
  },
  Mutation : {
    createUser : async (parent, args, context, info) => {
      {...}
    },
  },
} ;

Mais nous n'avons pas de données... Corrigeons cela 😉

N'oubliez pas d'importer la définition de type et le résolveur sur le serveur.

// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda" ;
import { ApolloServer } from "apollo-server" ;

import { typeDefs } from "./schemas" ;
import { resolvers } from "./resolvers" ;

{...}

Se connecter à MongoDB via mongoose

Il est maintenant temps de créer une connexion avec notre base de données. Dans ce cas particulier, il s'agit de MongoDB. C'est une base de données gratuite et facile à maintenir. Mais avant cela, installons deux autres dépendances :

npm install --save mongoose dotenv

La première étape consiste à créer un modèle d'utilisateur.

// src/model.ts
import mongoose, { Document, Error, Schema } from "mongoose" ;

export type User = Document & {
  _id : string,
  email : string,
  name : chaîne de caractères,
  password : chaîne de caractères,
} ;

supprimer mongoose.connection.models["User"] ;

const UserSchema : Schema = new Schema({
  email : {
    type : String,
    requis : true,
    unique : true,
  },
  name : {
    type : Chaîne,
    required : true,
    minLength : 3,
    maxLength : 32,
  },
  password : {
    type : String,
    required : true,
  },
}) ;

export const userModel = mongoose.model  ("User", UserSchema) ;

Rendre les mots de passe plus sûrs

La sécurité avant tout ! Sécurisons maintenant nos mots de passe en les hachant.

npm install --save bcrypt @types/bcrypt

Maintenant, implémentez la sécurité du mot de passe dans le callback du pre-middleware. Les fonctions pre-middleware sont exécutées l'une après l'autre, lors de l'appel suivant de chaque middleware. Pour sécuriser les mots de passe, nous utilisons une technique qui génère un sel et un hachage lors d'appels de fonction distincts.

// src/model.ts
import bcrypt from "bcrypt" ;
{...}
const SALT_WORK_FACTOR : nombre = 10 ;

UserSchema.pre("save", function (next) {
  const user = this as User ;
  if (!this.isModified("password")) return next() ;

  bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) {
    if (err) return next(err) ;

    bcrypt.hash(user.password, salt, function (err, hash) {
      if (err) return next(err) ;
      user.password = hash ;
      next() ;
    }) ;
  }) ;
}) ;
{...}

Ajoutez ensuite la méthode comparePasswords à UserSchema :

// src/model.ts
{...}
UserSchema.methods.comparePasswords = function (
  candidatePassword : string,
  cb : (err : Error | null, same : boolean | null) => void
) {
  const user = this as User ;
  bcrypt.compare(candidatePassword, user.password, (err, isMatch) => {
    if (err) {
      return cb(err, null) ;
    }
    cb(null, isMatch) ;
  }) ;
} ;
{...}

Et bien sûr, modifier le type d'utilisateur.

type comparePasswordFunction = (
  candidatePassword : string,
  cb : (err : Error, isMatch : boolean) => void
) => void ;

export type User = Document & {
  id : chaîne de caractères,
  email : string,
  name : chaîne de caractères,
  password : chaîne de caractères,
  comparePasswords : comparePasswordFunction,
} ;

Nous pouvons maintenant établir une connexion entre les serveurs et les bases de données. MONGODB_URI est une variable d'environnement qui contient une chaîne de connexion nécessaire pour créer la connexion. Vous pouvez l'obtenir à partir de votre cluster panel après vous être connecté à votre compte MongoDB atlas. Placez-la à l'intérieur de .env

// .env
MONGODB_URI = ... ;

N'oubliez jamais d'ajouter ce fichier à .gitignore. Super ! Ajoutons maintenant une fonction qui permet de se connecter à la base de données.

// src/server.ts
import mongoose, { Connection } from "mongoose" ;
{...}
let cachedDb : Connexion ;

const connectToDatabase = async () => {
  if (cachedDb) return ;

  await mongoose.connect(process.env.MONGODB_URI || "", {
    useNewUrlParser : true,
    useUnifiedTopology : true,
    useFindAndModify : false,
    useCreateIndex : true,
  }) ;
  cachedDb = mongoose.connection ;
} ;
{...}

Le contexte est un objet partagé par tous les résolveurs. Pour le fournir, il suffit d'ajouter une fonction d'initialisation du contexte au constructeur d'ApolloServer. C'est ce que nous allons faire.

// src/server.ts
import { userModel } from "./models/user.model" ;
{...}
const createLambdaServer = async () =>
  nouveau ApolloServerLambda({
    typeDefs,
    resolvers,
    introspection : true,
    terrain de jeu : true,
    context : async () => {
      attend connectToDatabase() ;

      return {
        modèles : {
          userModel,
        },
      } ;
    },
  }) ;

const createLocalServer = () =>
  nouveau ApolloServer({
    typeDefs,
    résolveurs,
    introspection : true,
    terrain de jeu : true,
    context : async () => {
      attend connectToDatabase() ;

      return {
        modèles : {
          userModel,
        },
      } ;
    }
  }) ;

Comme vous pouvez le constater, nous transmettons également userModel à travers contexte. Nous pouvons maintenant mettre à jour le résolveur :

// resolvers.ts
const userResolver = {
  Query : {
    user : async (_, { email, name, password }, { models : { userModel } }) => {
      const user = await userModel.findById({ _id : id }).exec() ;
      return user ;
    },
  },
  Mutation : {
    createUser : async (_, { id }, { models : { userModel } }) => {
      const user = await userModel.create({ email, name, password }) ;
      return user ;
    },
  },
} ;

Ça a l'air bien ! Créez maintenant une instance de serveur de base :

// src/index.ts
import { createLocalServer } from "./server" ;
require("dotenv").config() ;

const port = process.env.PORT || 4000 ;

const server = createLocalServer() ;

server.listen(port).then(({ url })) => {
  console.log(`Server ir running at ${url}`) ;
}) ;

Démarrer le serveur local avec Nodemon

Dernière chose avant l'exécution, ajouter nodemon.json

{
  "watch" : ["src"],
  "ext" : ".ts,.js",
  "ignore" : [],
  "exec" : "ts-node --transpile-only ./src/index.ts"
}

Ajouter un script à package.json

"scripts" : {
    "start" : "nodemon"
  },

Et courez !

npm start

Vous devriez obtenir ces informations à l'intérieur du terminal :

Le serveur ir fonctionne à l'adresse http://localhost:4000/

Si oui, ouvrez le http://localhost:4000/ dans votre navigateur.

Le terrain de jeu GraphQL devrait apparaître. Créons un nouvel utilisateur !

createUser.png

Regardez ce que cela donne dans la base de données.

databaseJohnDoe.png

Cool ! Tout fonctionne bien !

Essayons d'obtenir des informations sur l'utilisateur.

userWithoutAuth.png

Et ajouter le champ du mot de passe...

userPassword.png

Bien ! Nous recevons l'erreur Impossible d'interroger le champ "password" sur le type "User"".. Comme vous pouvez le constater, nous n'avons pas ajouté ce champ dans la définition du type d'utilisateur. Il est là à dessein. Nous ne devrions pas permettre l'interrogation d'un mot de passe ou d'autres données sensibles.

Autre chose... Nous pouvons obtenir les données de l'utilisateur sans aucune authentification... ce n'est pas une bonne solution. Nous devons y remédier.

Mais avant...

Configurer Codegen

Utilisons l'outil GraphQL code pour obtenir un type de base compatible, basé sur notre schéma.

npm install --save @graphql-codegen/cli @graphql-codegen/introspection
@graphql-codegen/typescript @graphql-codegen/typescript-resolvers

Créer codegen.yml

overwrite : true
schema : "http://localhost:4000"
génère :
  ./src/generated/graphql.ts :
    plugins :
      - "typescript"
      - "typescript-resolvers"
  ./graphql.schema.json :
    plugins :
      - "introspection"

Ajouter codegen pour package.json

"scripts" : {
    "start" : "nodemon",
    "codegen" : "graphql-codegen --config ./codegen.yml",
  },

Ensuite, lorsque le serveur local est en cours d'exécution, lancez le script :

npm run codegen

En cas de succès, vous recevrez un message :

  √ Analyse de la configuration
  √ Générer des sorties

Si vous recevez cette information, deux fichiers devraient apparaître :

  • graphql.schema.json dans le répertoire principal
  • graphql.ts dans le chemin nouvellement créé src/généré

C'est le second qui nous intéresse le plus. Si vous l'ouvrez, vous remarquerez une belle structure de types.

Nous pouvons maintenant améliorer nos résolveurs :

// src/resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql" ;

const userResolver : Resolvers = {
  Query : {
    user : async (_, { id }, { models : { userModel }, auth }) : Promise => {
      const user = await userModel.findById({ _id : id }).exec() ;
      return user ;
    },
  },
  Mutation : {
    createUser : async (
      _,
      { email, nom, mot de passe },
      { models : { userModel } }
    ) : Promise => {
      const user = await userModel.create({
        email,
        nom,
        mot de passe,
      }) ;
      return user ;
    },
  },
} ;

Authentification

Ensuite, nous allons mettre en place une authentification simple basée sur un jeton.

npm install --save jsonwebtoken @types/jsonwebtoken

Créer checkAuth pour vérifier si le jeton est valide. Nous ajouterons le résultat au contexte afin de pouvoir y accéder dans les résolveurs.

// src/server.ts
import { IncomingHttpHeaders } from "http";
import {
  APIGatewayProxyEvent,
  APIGatewayProxyEventHeaders,
  Context,
} from "aws-lambda";
import jwt from "jsonwebtoken";
{...}
const checkAuth = async ({ token } : APIGatewayProxyEventHeaders | IncomingHttpHeaders ) => {
  if (typeof token === "string") {
    try {
      return await jwt.verify(token, "riddlemethis");
    } catch (e) {
      throw new AuthenticationError(`Your session expired. Sign in again.`);
    }
  }
};
{...}
const createLambdaServer = async (
  { headers }: APIGatewayProxyEvent,
  context: Context
) => {
  return new ApolloServerLambda({
    typeDefs,
    resolvers,
    context: async () => {
      await connectToDatabase();

      const auth = await checkAuth(headers);

      return {
        auth,
        models: {
          userModel,
        },
      };
    },
  });
};

function createLocalServer() {
  return new ApolloServer({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    context: async ({ req: { headers } = {} }) => {
      const auth = await checkAuth(headers);
      await connectToDatabase();

      return {
         auth,
         models: {
          userModel,
        },
      };
    },
  });
}

Nous avons également besoin d'un moyen de créer un tel jeton. Le meilleur moyen est d'implémenter une requête de login dans notre résolveur.

// resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql" ;

const userResolver : Resolvers = {
  Query : {
    user : async (_, { id }, { models : { userModel }, auth }) : Promise => {
      if (!auth) throw new AuthenticationError("Vous n'êtes pas authentifié") ;

      const user = await userModel.findById({ _id : id }).exec() ;
      return user ;
    },
    login : async (
      _,
      { email, password },
      { models : { userModel } }
    ) : Promise => {
      const user = await userModel.findOne({ email }).exec() ;

      if (!user) throw new AuthenticationError("Invalid credentials") ;

      const matchPasswords = bcrypt.compareSync(password, user.password) ;

      if (!matchPasswords) throw new AuthenticationError("Invalid credentials") ;

      const token = jwt.sign({ id : user.id }, "riddlemethis", {
        expiresIn : 60,
      }) ;

      return { token } ;
    },
  },
  Mutation : {
    createUser : async (
      _,
      { email, nom, mot de passe },
      { models : { userModel } }
    ) : Promise => {
      const user = await userModel.create({
        email,
        nom,
        mot de passe,
      }) ;
      return user ;
    },
  },
} ;

Vous devez également mettre à jour les définitions des types d'utilisateurs en fonction du type de jeton et de la requête de connexion.

type Token {
    token : Chaîne !
  }

type Query {
    user(id : ID !): Utilisateur !
    login(email : String !, password : String !): Token !
  }

Essayons maintenant d'obtenir l'utilisateur sans jeton

noAuth.png

OK, ça marche ! Essayer de se connecter

jeton.png

Essayons à nouveau d'obtenir l'utilisateur, mais cette fois-ci avec un jeton ajouté aux en-têtes

avecToken.png

Cool ! Et si nous nous trompons d'identifiants ?

wrongEmail.png

C'est bien !

Préparer le déploiement

Dernière étape : déployer l'api sans serveur avec Netlify !

Créer un dossier lambda dans le répertoire principal et y placer deux fichiers :

Le premier contient AWS . Il crée une instance de serveur ApolloServerLambda et
puis exposer un gestionnaire en utilisant createHandler de cette instance.

// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda" ;
import { createLambdaServer } from " ???";

export const handler = async (
  event : APIGatewayProxyEvent,
  context : Contexte
) => {
  const server = await createLambdaServer(event, context) ;

  return new Promise((res, rej) => {
    const cb = (err : Error, args : any) => (err ? rej(err) : res(args)) ;
    server.createHandler()(event, context, cb) ;
  }) ;
} ;

Vous pouvez en savoir plus à ce sujet.

Le deuxième est le tsconfig. La partie importante est le outDir champ.

// lambda/tsconfig.json
{
  "compilerOptions" : {
    "sourceMap" : true,
    "noImplicitAny" : true,
    "module" : "commonjs",
    "target" : "es6",
    "experimentalDecorators" : true,
    "allowSyntheticDefaultImports" : true,
    "moduleResolution" : "nœud",
    "skipLibCheck" : true,
    "esModuleInterop" : true,
    "outDir" : "./dist"
  },
  "include" : ["./*.ts", "./**/*.ts", "./**/*.js"]
}

Mais il y a un problème. Nous ne pouvons pas utiliser /src car Netlify ne peut pas aller au-delà du dossier lambda. Nous devons donc l'empaqueter.

npm install --save ncp

Il s'agit d'un logiciel qui permet de copier un répertoire.

Ajouter liasse pour package.json

"scripts" : {
    "bundle" : "ncp ./src ./lambda/bundle",
    "codegen" : "graphql-codegen --config ./codegen.yml",
    "start" : "nodemon",
  },

Maintenant, si vous exécutez npm run bundleVous pouvez constater que dans les lambda/bundle nous avons tous les fichiers de src/.

Mettre à jour le chemin d'importation à l'intérieur de lambda/graphql.ts

// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda" ;
import { createLambdaServer } from "./bundle/server" ;

{...}

Vous pouvez maintenant ajouter le répertoire lambda/bundle à .gitignore

Déployer avec Netlify

Nous devons indiquer à Netlify quelle est la commande de construction et où se trouvent nos fonctions. Pour cela, créons netlify.toml file:

// netlify.toml
[build]
  command = "npm run build:lambda"
  functions = "lambda/dist"

Comme vous pouvez le constater, il s'agit du même répertoire que celui défini en tant que outDir champ en lambda/tsconfig.json

Voici à quoi devrait ressembler la structure de votre application (enfin... une partie ;))

app
└───lambda
│ └────bundle
│ │ graphql.ts
│ tsconfig.json.ts
└───src
└────generated
│ │ │ graphql.ts
│ │ index.ts
│ │ model.ts
│ │ resolvers.ts
│ │ schemas.ts
│ server.ts
│
│ codegen.yml
│ graphql.schema.json
│ netlify.toml
│ nodemon.json
│ tsconfig.json

Ajouter bundle:lambda pour package.json

"scripts" : {
    "build:lambda" : "npm run bundle && tsc -p lambda/tsconfig.json",
    "bundle" : "ncp ./src ./lambda/bundle",
    "codegen" : "graphql-codegen --config ./codegen.yml",
    "start" : "nodemon",
  },

Déployer

Essayons de déployer notre application via Netlify.

  1. Connectez-vous à votre compte Netlify,
  2. Connectez-vous à votre Github,
  3. Cliquez sur le bouton "Nouveau site à partir de Git",
  4. Sélectionnez un répertoire approprié,
  5. Définir la commande de construction npm run build:lambda,
  6. Ajouter une variable d'environnement (MONGODB_URI),
  7. Déployer...

Et TAAADAAAA...

pageNotFound.png

En effet, le point de terminaison n'est pas, par défaut, le point de terminaison http://page/ mais http://page/.netlify/functions/graphql.

sansRedirect.png

Comment y remédier ? C'est très simple. Il suffit de créer redirections avec :

/ /.netlify/functions/graphql 200 !

Déployez à nouveau et vérifiez.

redirect.png

J'espère qu'il vous plaira ! N'hésitez pas à l'améliorer et à le modifier.

En savoir plus :

Comment ne pas tuer un projet avec de mauvaises pratiques de codage ?

Sécurité des applications web. Vulnérabilité Target="_blank

Sécurité des applications web - vulnérabilité XSS

Articles connexes

Développement de logiciels

Construire des applications web à l'épreuve du temps : les conseils de l'équipe d'experts de The Codest

Découvrez comment The Codest excelle dans la création d'applications web évolutives et interactives à l'aide de technologies de pointe, offrant une expérience utilisateur transparente sur toutes les plateformes. Découvrez comment notre expertise favorise la transformation numérique et la...

LE CODEST
Développement de logiciels

Les 10 premières entreprises de développement de logiciels basées en Lettonie

Découvrez les principales sociétés de développement de logiciels en Lettonie et leurs solutions innovantes dans notre dernier article. Découvrez comment ces leaders de la technologie peuvent vous aider à développer votre entreprise.

thecodest
Solutions pour les entreprises et les grandes entreprises

L'essentiel du développement de logiciels Java : Un guide pour une externalisation réussie

Explorez ce guide essentiel sur le développement réussi de logiciels Java outsourcing pour améliorer l'efficacité, accéder à l'expertise et assurer la réussite des projets avec The Codest.

thecodest
Développement de logiciels

Le guide ultime de l'externalisation en Pologne

L'essor de outsourcing en Pologne est dû aux progrès économiques, éducatifs et technologiques, qui favorisent la croissance des technologies de l'information et un climat propice aux entreprises.

TheCodest
Solutions pour les entreprises et les grandes entreprises

Le guide complet des outils et techniques d'audit informatique

Les audits informatiques garantissent la sécurité, l'efficacité et la conformité des systèmes. Pour en savoir plus sur leur importance, lisez l'article complet.

The Codest
Jakub Jakubowicz CTO & Co-Fondateur

Abonnez-vous à notre base de connaissances et restez au courant de l'expertise du secteur des technologies de l'information.

    A propos de nous

    The Codest - Entreprise internationale de développement de logiciels avec des centres technologiques en Pologne.

    Royaume-Uni - Siège

    • Bureau 303B, 182-184 High Street North E6 2JA
      Londres, Angleterre

    Pologne - Les pôles technologiques locaux

    • Parc de bureaux Fabryczna, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Varsovie, Pologne

      The Codest

    • Accueil
    • A propos de nous
    • Services
    • Études de cas
    • Savoir comment
    • Carrières
    • Dictionnaire

      Services

    • Conseil consultatif
    • Développement de logiciels
    • Développement backend
    • Développement frontal
    • Staff Augmentation
    • Développeurs backend
    • Ingénieurs en informatique dématérialisée
    • Ingénieurs des données
    • Autres
    • Ingénieurs AQ

      Ressources

    • Faits et mythes concernant la coopération avec un partenaire externe de développement de logiciels
    • Des États-Unis à l'Europe : Pourquoi les startups américaines décident-elles de se délocaliser en Europe ?
    • Comparaison des pôles de développement Tech Offshore : Tech Offshore Europe (Pologne), ASEAN (Philippines), Eurasie (Turquie)
    • Quels sont les principaux défis des CTO et des DSI ?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Conditions d'utilisation du site web

    Copyright © 2025 par The Codest. Tous droits réservés.

    fr_FRFrench
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek fr_FRFrench