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 już istnieje') } 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 }) }, } } })() Wdrażanie API GraphQL/MongoDB przy użyciu funkcji Netlify - The Codest
The Codest
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Branże
    • Fintech i bankowość
    • E-commerce
    • Adtech
    • Healthtech
    • Produkcja
    • Logistyka
    • Motoryzacja
    • IOT
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
Strzałka w tył WSTECZ
2021-05-13
Software Development

Wdrażanie API GraphQL/MongoDB przy użyciu funkcji Netlify

The Codest

Paweł Rybczyński

Software Engineer

Cele Początkowa konfiguracja Zainstaluj zależności Zacznijmy Najpierw dodaj tsconfig.json do katalogu głównego: Teraz stwórzmy src/server.ts dla implementacji serwerów. Następnie dodajmy dwie funkcje: jedną dla serwera lokalnego i drugą dla lambdy. OK, nie mamy żadnych resolverów ani definicji typów, więc musimy je stworzyć. Załóżmy, że na początku chcemy [...]

Cele

  1. Skonfiguruj zarówno serwer lokalny, jak i serwer lambda.
  2. Podłącz oba do MongoDB.
  3. Wdrożenie podstawowego uwierzytelniania.
  4. Wdrożenie bezserwerowego Apollo GraphQL API z Netlify.
  5. Korzystanie z Typescript.

Konfiguracja początkowa

npm init -y

Instalowanie zależności

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

Zacznijmy

Najpierw dodaj tsconfig.json do katalogu głównego:

 {
 "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"]
 }

Teraz stwórzmy src/server.ts dla implementacji serwerów. Następnie dodaj dwie funkcje: jedną dla lokalnego serwera i drugą dla lambdy.

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


const createLambdaServer = () =>
  new ApolloServerLambda({
    typeDefs,
    resolvers,
    introspekcja: true,
    playground: true,
    },
  });

const createLocalServer = () =>
  new ApolloServer({
    typeDefs,
    resolvers,
    introspekcja: true,
    playground: true,
    },
  });

export { createLambdaServer, createLocalServer };

OK, nie mamy żadnych resolverów ani definicji typów, więc musimy je stworzyć. Załóżmy, że na początku chcemy tworzyć użytkowników i otrzymywać informacje o nich.

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

const userSchema = gql`
  type Użytkownik {
    id: ID!
    email: String!
    name: String!
  }

  type Query {
    user(id: ID!): Użytkownik!
  }

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

Jeśli nie jesteś z tym zaznajomiony, Apollo przygotował bardzo fajny tutorial

Teraz stwórzmy resolver użytkownika z jednym zapytaniem i jedną mutacją.

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

Ale nie mamy żadnych danych... Naprawmy to 😉

Nie zapomnij zaimportować definicji typu i resolvera na serwer.

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

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

{...}

Połączenie z MongoDB za pośrednictwem mongoose

Teraz jest dobry moment na utworzenie połączenia z naszą bazą danych. W tym konkretnym przypadku będzie to MongoDB. Jest darmowa i łatwa w utrzymaniu. Ale zanim to nastąpi, zainstalujmy jeszcze dwie zależności:

npm install --save mongoose dotenv

Pierwszym krokiem jest utworzenie modelu użytkownika.

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

export type User = Document & {
  _id: string,
  email: string,
  name: string,
  hasło: ciąg znaków,
};

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

const UserSchema: Schema = new Schema({
  email: {
    type: String,
    required: true
    unique: true,
  },
  name: {
    type: String,
    required: true,
    minLength: 3,
    maxLength: 32,
  },
  hasło: {
    type: String,
    required: true,
  },
});

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

Zwiększ bezpieczeństwo haseł

Bezpieczeństwo przede wszystkim! Zabezpieczmy teraz nasze hasła poprzez ich haszowanie.

npm install --save bcrypt @types/bcrypt

Teraz zaimplementuj zabezpieczenie hasłem wewnątrz wywołania zwrotnego pre-middleware. Funkcje pre-middleware są wykonywane jedna po drugiej, gdy każde oprogramowanie pośredniczące wywołuje następne. Aby hasła były bezpieczne, używamy techniki, która generuje sól i hash w oddzielnych wywołaniach funkcji.

// src/model.ts
import bcrypt from "bcrypt";
{...}
const SALT_WORK_FACTOR: number = 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();
    });
  });
});
{...}

Następnie dodaj metodę comparePasswords do 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);
  });
};
{...}

I oczywiście zmodyfikuj typ użytkownika.

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

export type User = Document & {
  _id: string,
  email: string,
  name: string,
  password: string,
  comparePasswords: comparePasswordFunction,
};

Teraz możemy zorganizować połączenie między serwerami i bazami danych. MONGODB_URI to zmienna środowiskowa, która zawiera ciąg połączenia potrzebny do utworzenia połączenia. Można go uzyskać z panelu klastra po zalogowaniu się na konto atlas MongoDB. Umieść ją wewnątrz .env

// .env
MONGODB_URI = ...;

Zawsze należy pamiętać o dodaniu tego pliku do .gitignore. Świetnie! Teraz dodajmy funkcję, która pozwoli połączyć się z db.

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

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

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

Kontekst jest obiektem współdzielonym przez wszystkie resolvery. Aby go udostępnić, wystarczy dodać funkcję inicjalizacji kontekstu do konstruktora ApolloServer. Zróbmy to.

// src/server.ts
import { userModel } z "./models/user.model";
{...}
const createLambdaServer = async () =>
  new ApolloServerLambda({
    typeDefs,
    resolvers,
    introspekcja: true,
    playground: true,
    context: async () => {
      await connectToDatabase();

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

const createLocalServer = () =>
  new ApolloServer({
    typeDefs,
    resolvers,
    introspekcja: true,
    playground: true,
    context: async () => {
      await connectToDatabase();

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

Jak widać, przekazujemy również userModel poprzez kontekst. Możemy teraz zaktualizować resolver:

// 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;
    },
  },
};

Wygląda ładnie! Teraz utwórz podstawową instancję serwera:

// src/index.ts
import { createLocalServer } z "./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}`);
});

Uruchamianie lokalnego serwera za pomocą Nodemona

Ostatnia rzecz przed uruchomieniem, dodaj nodemon.json

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

Dodaj skrypt do package.json

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

I biegnij!

npm start

Powinieneś otrzymać takie informacje w terminalu:

Serwer działa pod adresem http://localhost:4000/

Jeśli tak, otwórz http://localhost:4000/ wewnątrz przeglądarki.

Powinien pojawić się plac zabaw GraphQL. Utwórzmy nowego użytkownika!

createUser.png

Zobacz, jak to wygląda w bazie danych.

databaseJohnDoe.png

Super! Wszystko działa jak należy!

Spróbujmy uzyskać informacje o użytkowniku.

userWithoutAuth.png

I dodaj pole hasła...

userPassword.png

Nieźle! Otrzymujemy błąd Nie można wykonać zapytania w polu "hasło" dla typu "Użytkownik".". Jak możesz sprawdzić, nie dodaliśmy tego pola wewnątrz definicji typu użytkownika. Jest tam celowo. Nie powinniśmy umożliwiać zapytania o hasło lub inne wrażliwe dane.

Kolejna rzecz... Możemy uzyskać dane użytkownika bez żadnego uwierzytelniania... to nie jest dobre rozwiązanie. Musimy to naprawić.

Ale zanim...

Konfiguracja Codegen

Użyjmy GraphQL kod aby uzyskać kompatybilny typ bazowy, oparty na naszym schemacie.

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

Utwórz codegen.yml

overwrite: true
schema: "http://localhost:4000"
generates:
  ./src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
  ./graphql.schema.json:
    plugins:
      - "introspection"

Dodaj kodegen skrypt do package.json

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

Następnie, gdy lokalny serwer jest uruchomiony, uruchom skrypt:

npm run codegen

W przypadku powodzenia otrzymasz wiadomość:

  √ Analizowanie konfiguracji
  √ Generowanie wyjść

Jeśli otrzymasz tę informację, powinny pojawić się dwa pliki:

  • graphql.schema.json w katalogu głównym
  • graphql.ts w nowo utworzonej ścieżce src/generated

Jesteśmy bardziej zainteresowani tym drugim. Jeśli go otworzysz, zauważysz ładną strukturę typów.

Teraz możemy ulepszyć nasze resolvery:

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

const userResolver: Resolvers = {
  Query: {
    user: async (_, { id }, { models: { userModel }, auth }): Promise => {
      const user = await userModel.findById({ _id: id }).exec();
      return user;
    },
  },
  Mutacja: {
    createUser: async (
      _,
      { email, name, password },
      { models: { userModel } }
    ): Promise => {
      const user = await userModel.create({
        email,
        name,
        hasło,
      });
      return user;
    },
  },
};

Uwierzytelnianie

Następnie skonfigurujmy proste uwierzytelnianie oparte na tokenie.

npm install --save jsonwebtoken @types/jsonwebtoken

Utwórz checkAuth aby sprawdzić, czy token jest prawidłowy. Dodamy wynik do kontekstu, abyśmy mogli uzyskać do niego dostęp wewnątrz resolverów.

// 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,
        },
      };
    },
  });
}

Potrzebujemy również sposobu na utworzenie takiego tokena. Najlepszym sposobem jest zaimplementowanie zapytania logowania wewnątrz naszego resolvera.

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

const userResolver: Resolvers = {
  Query: {
    user: async (_, { id }, { models: { userModel }, auth }): Promise => {
      if (!auth) throw new AuthenticationError("Nie jesteś uwierzytelniony");

      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("Nieprawidłowe poświadczenia");

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

      return { token };
    },
  },
  Mutacja: {
    createUser: async (
      _,
      { email, name, password },
      { models: { userModel } }
    ): Promise => {
      const user = await userModel.create({
        email,
        name,
        hasło,
      });
      return user;
    },
  },
};

Należy również zaktualizować definicje typów użytkowników według typu tokena i zapytania logowania.

typ Token {
    token: String!
  }

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

Spróbujmy teraz pobrać użytkownika bez tokena

noAuth.png

OK, działa dobrze! Spróbuj się zalogować

token.png

Spróbujmy ponownie uzyskać użytkownika, ale tym razem z tokenem dodanym do nagłówków

withToken.png

Super! A co jeśli ustawimy złe poświadczenia?

wrongEmail.png

Nieźle!

Przygotowanie do wdrożenia

Ostatni krok: wdrożenie bezserwerowego API za pomocą Netlify!

Utwórz folder lambda w katalogu głównym i umieść w nim dwa pliki:

Pierwszy zawiera AWS handler. Tworzy on instancję serwera ApolloServerLambda i
a następnie wystawić procedurę obsługi za pomocą createHandler tej instancji.

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

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  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);
  });
};

Możesz przeczytać więcej na ten temat.

Drugą częścią jest tsconfig. Ważną częścią jest outDir pole.

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

Jest jednak pewien problem. Nie możemy użyć /src ponieważ Netlify nie może dotrzeć poza folder lambda. Musimy więc go spakować.

npm install --save ncp

Jest to pakiet umożliwiający kopiowanie katalogów.

Dodaj pakiet skrypt do package.json

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

Teraz, jeśli uruchomisz npm run bundlemożna zobaczyć, że w nowo utworzonych lambda/bundle dir mamy wszystkie pliki z src/.

Zaktualizuj ścieżkę importu wewnątrz lambda/graphql.ts

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

{...}

Możesz teraz dodać lambda/bundle dir do .gitignore

Wdrożenie za pomocą Netlify

Musimy powiedzieć Netlify, jakie jest polecenie kompilacji i gdzie znajdują się nasze funkcje. Aby to zrobić, stwórzmy netlify.toml file:

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

Jak widać, jest to ten sam katalog, który został zdefiniowany jako outDir pole w lambda/tsconfig.json

Tak powinna wyglądać struktura aplikacji (no... jej część ;)).

aplikacja
└───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

Dodaj bundle:lambda skrypt do 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",
  },

Wdrożenie

Spróbujmy wdrożyć naszą aplikację za pośrednictwem Netlify.

  1. Zaloguj się do swojego konta Netlify,
  2. Połącz się ze swoim Githubem,
  3. Kliknij przycisk "Nowa witryna z Git",
  4. Wybierz odpowiednie repozytorium,
  5. Ustaw polecenie kompilacji npm run build:lambda,
  6. Dodaj zmienną środowiskową (MONGODB_URI),
  7. Wdrożenie...

I TAAADAAAA...

pageNotFound.png

Dzieje się tak, ponieważ punkt końcowy domyślnie nie jest punktem http://page/ ale http://page/.netlify/functions/graphql.

withoutRedirect.png

Jak to naprawić? To bardzo proste. Wystarczy utworzyć przekierowania z:

/ /.netlify/functions/graphql 200!

Wdróż ponownie i sprawdź.

redirect.png

Mam nadzieję, że się podobało! Zapraszam do poprawek i zmian.

Czytaj więcej:

Jak nie zabić projektu złymi praktykami kodowania?

Bezpieczeństwo aplikacji internetowych. Target="_blank" podatność na ataki

Bezpieczeństwo aplikacji webowych - luka XSS

Powiązane artykuły

Software Development

Tworzenie przyszłościowych aplikacji internetowych: spostrzeżenia zespołu ekspertów The Codest

Odkryj, w jaki sposób The Codest wyróżnia się w tworzeniu skalowalnych, interaktywnych aplikacji internetowych przy użyciu najnowocześniejszych technologii, zapewniając płynne doświadczenia użytkowników na wszystkich platformach. Dowiedz się, w jaki sposób nasza wiedza napędza transformację cyfrową i biznes...

THEECODEST
Software Development

10 najlepszych firm tworzących oprogramowanie na Łotwie

Dowiedz się więcej o najlepszych łotewskich firmach programistycznych i ich innowacyjnych rozwiązaniach w naszym najnowszym artykule. Odkryj, w jaki sposób ci liderzy technologiczni mogą pomóc w rozwoju Twojej firmy.

thecodest
Rozwiązania dla przedsiębiorstw i scaleupów

Podstawy tworzenia oprogramowania Java: Przewodnik po skutecznym outsourcingu

Zapoznaj się z tym niezbędnym przewodnikiem na temat skutecznego tworzenia oprogramowania Java outsourcing, aby zwiększyć wydajność, uzyskać dostęp do wiedzy specjalistycznej i osiągnąć sukces projektu z The Codest.

thecodest
Software Development

Kompletny przewodnik po outsourcingu w Polsce

Wzrost liczby outsourcing w Polsce jest napędzany przez postęp gospodarczy, edukacyjny i technologiczny, sprzyjający rozwojowi IT i przyjazny klimat dla biznesu.

TheCodest
Rozwiązania dla przedsiębiorstw i scaleupów

Kompletny przewodnik po narzędziach i technikach audytu IT

Audyty IT zapewniają bezpieczne, wydajne i zgodne z przepisami systemy. Dowiedz się więcej o ich znaczeniu, czytając cały artykuł.

The Codest
Jakub Jakubowicz CTO & Współzałożyciel

Subskrybuj naszą bazę wiedzy i bądź na bieżąco!

    O nas

    The Codest - Międzynarodowa firma programistyczna z centrami technologicznymi w Polsce.

    Wielka Brytania - siedziba główna

    • Office 303B, 182-184 High Street North E6 2JA
      Londyn, Anglia

    Polska - lokalne centra technologiczne

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warszawa, Polska

      The Codest

    • Strona główna
    • O nas
    • Nasze Usługi
    • Case Studies
    • Nasze Know How
    • Kariera
    • Słownik

      Nasze Usługi

    • Konsultacje IT
    • Software Development
    • Backend Development
    • Frontend Development
    • Zespoły IT
    • Backend Dev
    • Inżynierowie rozwiązań chmurowych
    • Inżynierowie danych
    • Inne
    • Inżynierowie QA

      Raporty

    • Fakty i mity na temat współpracy z zewnętrznym partnerem programistycznym
    • Z USA do Europy: Dlaczego amerykańskie startupy decydują się na relokację do Europy?
    • Porównanie centrów rozwoju Tech Offshore: Tech Offshore Europa (Polska), ASEAN (Filipiny), Eurazja (Turcja)
    • Jakie są największe wyzwania CTO i CIO?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Warunki korzystania z witryny

    Copyright © 2025 by The Codest. Wszelkie prawa zastrzeżone.

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