Codest
  • Tietoa meistä
  • Palvelut
    • Ohjelmistokehitys
      • Frontend-kehitys
      • Backend-kehitys
    • Staff Augmentation
      • Frontend-kehittäjät
      • Backend-kehittäjät
      • Tietoinsinöörit
      • Pilvi-insinöörit
      • QA insinöörit
      • Muut
    • Se neuvoa-antava
      • Tilintarkastus & konsultointi
  • Toimialat
    • Fintech & pankkitoiminta
    • E-commerce
    • Adtech
    • Terveysteknologia
    • Valmistus
    • Logistiikka
    • Autoteollisuus
    • IOT
  • Arvo
    • TOIMITUSJOHTAJA
    • CTO
    • Toimituspäällikkö
  • Tiimimme
  • Tapaustutkimukset
  • Tiedä miten
    • Blogi
    • Tapaamiset
    • Webinaarit
    • Resurssit
Työurat Ota yhteyttä
  • Tietoa meistä
  • Palvelut
    • Ohjelmistokehitys
      • Frontend-kehitys
      • Backend-kehitys
    • Staff Augmentation
      • Frontend-kehittäjät
      • Backend-kehittäjät
      • Tietoinsinöörit
      • Pilvi-insinöörit
      • QA insinöörit
      • Muut
    • Se neuvoa-antava
      • Tilintarkastus & konsultointi
  • Arvo
    • TOIMITUSJOHTAJA
    • CTO
    • Toimituspäällikkö
  • Tiimimme
  • Tapaustutkimukset
  • Tiedä miten
    • Blogi
    • Tapaamiset
    • Webinaarit
    • Resurssit
Työurat Ota yhteyttä
Takaisin nuoli PALAA TAAKSE
2021-05-13
Ohjelmistokehitys

GraphQL/MongoDB API:n käyttöönotto Netlify Functionsin avulla

Codest

Pawel Rybczynski

Software Engineer

Tavoitteet Alkuasetukset Riippuvuuksien asentaminen Aloitetaan Aluksi lisätään tsconfig.json päähakemistoon: Nyt luodaan src/server.ts palvelintoteutuksia varten. Lisää sitten kaksi funktiota: toinen paikalliselle palvelimelle ja toinen lambdalle. OK, meillä ei ole resolvereita tai tyyppimäärityksiä, joten meidän on luotava niitä. Oletetaan, että aluksi haluamme [...]

Tavoitteet

  1. Määritä sekä paikalliset että lambda-palvelimet.
  2. Yhdistä molemmat MongoDB:hen.
  3. Ota käyttöön perustodennus.
  4. Ota palvelimetön Apollo käyttöön GraphQL API Netlifyn kanssa.
  5. Typescriptin käyttö.

Alkuasetukset

npm init -y

Asenna riippuvuudet

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

Aloitetaan

Lisää ensin tsconfig.json päähakemistoon:

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

Luodaan nyt src/server.ts palvelintoteutuksia varten. Lisää sitten kaksi funktiota: toinen paikalliselle palvelimelle ja toinen lambdalle.

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


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

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

export { createLambdaServer, createLocalServer };

OK, meillä ei ole resolvereita tai tyyppimäärityksiä, joten meidän on luotava niitä. Oletetaan, että aluksi haluamme luoda käyttäjiä ja vastaanottaa tietoja heistä.

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

const userSchema = gql`
  type Käyttäjä {
    id: ID!
    email: String!
    name: String!
  }

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

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

Jos tämä ei ole sinulle tuttu, Apollo valmis erittäin mukava opetusohjelma

Luodaan nyt käyttäjän resolveri, jossa on yksi kysely ja yksi mutaatio.

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

Mutta meillä ei ole mitään tietoja... Korjataan se 😉.

Älä unohda tuoda tyyppimäärittelyä ja resolveria palvelimelle.

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

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

{...}

Yhteys MongoDB:hen mongoose:n kautta

Nyt on hyvä aika luoda yhteys tietokantaan. Tässä tapauksessa se on MongoDB. Se on ilmainen ja helppo ylläpitää. Mutta ennen sitä asennetaan vielä kaksi riippuvuutta:

npm install --save mongoose dotenv

Ensimmäinen vaihe on luoda käyttäjämalli.

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

export type User = Document & {
  _id: string,
  email: string,
  name: string,
  salasana: string,
};

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

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

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

Tee salasanoista turvallisempia

Turvallisuus ensin! Suojaamme nyt salasanamme hashtaamalla ne.

npm install --save bcrypt @types/bcrypt

Toteuta nyt salasanaturvallisuus pre-middleware-kutsun takaisinkutsun sisällä. Pre-middleware-funktiot suoritetaan peräkkäin, kun kukin middleware kutsuu seuraavaa. Jotta salasanat olisivat turvallisia, käytämme tekniikkaa, joka luo suolan ja hashin erillisillä funktiokutsuilla.

// 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();
    });
  });
});
{...}

Lisää sitten comparePasswords-menetelmä UserSchemaan:

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

Ja tietysti muokkaa Käyttäjätyyppiä.

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

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

Nyt voimme järjestää yhteyden palvelimien ja tietokantojen välille. MONGODB_URI on ympäristömuuttuja, joka sisältää yhteyden luomiseen tarvittavan merkkijonon. Saat sen klusteripaneelistasi, kun olet kirjautunut MongoDB-atlas-tilillesi. Laita se sisään .env

// .env
MONGODB_URI = ....;

Muista aina lisätä kyseinen tiedosto .gitignore. Hienoa! Nyt lisätään funktio, joka mahdollistaa yhteyden muodostamisen db:hen.

// 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;
};
{...}

Konteksti on objekti, joka on yhteinen kaikille resolvereille. Sen tarjoamiseksi meidän tarvitsee vain lisätä kontekstin alustustoiminto ApolloServer-konstruktoriin. Tehdään se.

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

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

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

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

Kuten näette, me myös välitämme userModel kautta konteksti. Voimme nyt päivittää resolverin:

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

Näyttää hyvältä! Luo nyt peruspalvelininstanssi:

// 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 käynnissä osoitteessa ${url}`);
});

Paikallisen palvelimen käynnistäminen Nodemonilla

Viimeinen asia ennen ajoa, lisää nodemon.json

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

Lisää skripti package.json

"skriptit": {
    "start": "nodemon"
  },

Ja juokse!

npm start

Sinun pitäisi saada nämä tiedot terminaalin sisältä:

Palvelin ir toimii osoitteessa http://localhost:4000/

Jos kyllä, avaa http://localhost:4000/ selaimesi sisällä.

GraphQL-leikkikentän pitäisi näkyä. Luodaan uusi käyttäjä!

createUser.png

Katso, miltä se näyttää tietokannassa.

databaseJohnDoe.png

Siistiä! Kaikki toimii hienosti!

Yritetään saada käyttäjätietoja.

userWithoutAuth.png

Ja lisää salasanakenttä...

userPassword.png

Hienoa! Saamme virheen Kenttää "salasana" ei voida kysyä tyypiltä "Käyttäjä".". Kuten voit tarkistaa, emme lisänneet tätä kenttää käyttäjätyypin määritelmän sisään. Se on siellä tarkoituksella. Meidän ei pitäisi mahdollistaa salasanan tai muiden arkaluonteisten tietojen kyselyä.

Toinen asia... Voimme saada käyttäjätietoja ilman todennusta... se ei ole hyvä ratkaisu. Meidän on korjattava se.

Mutta ennen kuin...

Codegenin määrittäminen

Käytetään GraphQL koodi generaattorilla saadaksemme yhteensopivan perustyypin, joka perustuu skeemaamme.

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

Luo codegen.yml

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

Lisää codegen skripti package.json

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

Kun paikallinen palvelin on käynnissä, suorita skripti:

npm run codegen

Onnistuessasi saat viestin:

  √ Parse-konfiguraatio
  √ Tuotosten luominen

Jos saat nämä tiedot, kaksi tiedostoa pitäisi näkyä:

  • graphql.schema.json päähakemistossa
  • graphql.ts äskettäin luodussa polussa src/generated

Olemme enemmän kiinnostuneita toisesta. Jos avaat sen, huomaat hienon tyyppirakenteen.

Nyt voimme parantaa resolvereitamme:

// 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;
    },
  },
  Mutaatio: {
    createUser: async (
      _,
      { email, name, password },
      { models: { userModel } }
    ): Promise => {
      const user = await userModel.create({
        email,
        name,
        password,
      });
      return user;
    },
  },
};

Tunnistus

Seuraavaksi määritetään yksinkertainen token-pohjainen todennus.

npm install --save jsonwebtoken @types/jsonwebtoken jsonwebtoken

Luo checkAuth toiminto tarkistaa, onko merkki voimassa. Lisäämme tuloksen kontekstiin, jotta voimme käyttää sitä resolverien sisällä.

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

Tarvitsemme myös tavan luoda tällainen merkki. Paras tapa on toteuttaa kirjautumiskysely resolverimme sisällä.

// 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("Sinua ei ole todennettu");

      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 };
    },
  },
  Mutaatio: {
    createUser: async (
      _,
      { email, name, password },
      { models: { userModel } }
    ): Promise => {
      const user = await userModel.create({
        email,
        name,
        password,
      });
      return user;
    },
  },
};

Sinun on myös päivitettävä käyttäjätyyppimääritykset Token-tyypin ja kirjautumiskyselyn mukaan.

tyyppi Token {
    token: Merkkijono!
  }

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

Yritetään nyt saada käyttäjä ilman tunnusta.

noAuth.png

OK, toimii hienosti! Yritä kirjautua sisään

token.png

Yritetään saada käyttäjä uudelleen, mutta tällä kertaa otsikoihin on lisätty tunniste.

withToken.png

Siistiä! Entä jos asetamme väärät tunnukset?

wrongEmail.png

Hienoa!

Valmistele käyttöönotto

Viimeinen vaihe: ota palvelimetön api käyttöön Netlifyllä!

Luo kansio lambda päähakemistoon ja laita kaksi tiedostoa sen sisälle:

Ensimmäinen sisältää AWS käsittelijä. Se luo ApolloServerLambda-palvelininstanssin ja
ja aseta sitten käsittelijä näkyviin käyttämällä kyseisen instanssin createHandler-ohjelmaa.

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

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

Voit lukea siitä lisää.

Toinen on tsconfig. Tärkeä osa on outDir kenttä.

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

Mutta siinä on ongelma. Emme voi käyttää /src dir, koska Netlify ei pääse lambda-kansion ulkopuolelle. Meidän on siis niputettava se.

npm install --save ncp

Se on paketti, jonka avulla voit kopioida hakemiston.

Lisää niputtaa skripti package.json

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

Nyt jos suoritat npm run bundle, näet, että äskettäin luodussa lambda/bundle dir meillä on kaikki tiedostot osoitteesta src/.

Päivitä tuontipolku sisällä lambda/graphql.ts

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

{...}

Voit nyt lisätä lambda/bundle dir:n osoitteeseen .gitignore

Käyttöönotto Netlifyllä

Meidän on kerrottava Netlifylle, mikä build-komento on ja missä toimintomme sijaitsevat. Sitä varten luodaan netlify.toml file:

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

Kuten näette, se on sama hakemisto, joka on määritetty kuin outDir kenttä lambda/tsconfig.json

Sovelluksesi rakenteen pitäisi näyttää tältä (no... osittain siltä ;))

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

Lisää bundle:lambda skripti package.json

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

Ota käyttöön

Kokeillaan sovelluksemme käyttöönottoa Netlifyn kautta.

  1. Kirjaudu Netlify-tilillesi,
  2. Yhdistä Githubiin,
  3. Napsauta 'New site from Git' -painiketta,
  4. Valitse oikea repo,
  5. Aseta rakennuskomento npm run build:lambda,
  6. Lisää ympäristömuuttuja (MONGODB_URI),
  7. Ota käyttöön...

Ja TAAADAAAAAA...

pageNotFound.png

Tämä johtuu siitä, että päätepiste ei oletusarvoisesti ole päätepisteen http://page/ mutta http://page/.netlify/functions/graphql.

withoutRedirect.png

Miten se korjataan? Se on hyvin yksinkertaista. Luo vain _redirects kanssa:

/ /.netlify/functions/graphql 200!

Ota käyttöön uudelleen ja tarkista.

redirect.png

Toivottavasti pidit siitä! Voit vapaasti parantaa ja muuttaa.

Lue lisää:

Miten projektia ei saa tappaa huonoilla koodauskäytännöillä?

Web-sovellusten turvallisuus. Target="_blank" haavoittuvuus

Web-sovelluksen turvallisuus - XSS-haavoittuvuus

Aiheeseen liittyvät artikkelit

Ohjelmistokehitys

Tulevaisuuden web-sovellusten rakentaminen: The Codest:n asiantuntijatiimin näkemyksiä

Tutustu siihen, miten The Codest loistaa skaalautuvien, interaktiivisten verkkosovellusten luomisessa huipputeknologian avulla ja tarjoaa saumattomia käyttäjäkokemuksia kaikilla alustoilla. Lue, miten asiantuntemuksemme edistää digitaalista muutosta ja liiketoimintaa...

THECODEST
Ohjelmistokehitys

Top 10 Latviassa toimivaa ohjelmistokehitysyritystä

Tutustu Latvian parhaisiin ohjelmistokehitysyrityksiin ja niiden innovatiivisiin ratkaisuihin uusimmassa artikkelissamme. Tutustu siihen, miten nämä teknologiajohtajat voivat auttaa nostamaan liiketoimintaasi.

thecodest
Yritys- ja skaalausratkaisut

Java-ohjelmistokehityksen perusteet: A Guide to Outsourcing Successfully

Tutustu tähän keskeiseen oppaaseen Java-ohjelmistokehityksen onnistuneesta ulkoistamisesta tehokkuuden parantamiseksi, asiantuntemuksen saamiseksi ja projektin onnistumiseksi The Codestin avulla.

thecodest
Ohjelmistokehitys

Perimmäinen opas ulkoistamiseen Puolassa

Ulkoistamisen lisääntyminen Puolassa johtuu taloudellisesta, koulutuksellisesta ja teknologisesta kehityksestä, joka edistää tietotekniikan kasvua ja yritysystävällistä ilmapiiriä.

TheCodest
Yritys- ja skaalausratkaisut

Täydellinen opas IT-tarkastustyökaluihin ja -tekniikoihin

Tietotekniikan tarkastuksilla varmistetaan turvalliset, tehokkaat ja vaatimustenmukaiset järjestelmät. Lue lisää niiden merkityksestä lukemalla koko artikkeli.

Codest
Jakub Jakubowicz teknologiajohtaja ja toinen perustaja

Tilaa tietopankkimme ja pysy ajan tasalla IT-alan asiantuntemuksesta.

    Tietoa meistä

    The Codest - Kansainvälinen ohjelmistokehitysyritys, jolla on teknologiakeskuksia Puolassa.

    Yhdistynyt kuningaskunta - pääkonttori

    • Toimisto 303B, 182-184 High Street North E6 2JA
      Lontoo, Englanti

    Puola - Paikalliset teknologiakeskukset

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Krakova
    • Brain Embassy, Konstruktorska
      11, 02-673 Varsova, Puola

      Codest

    • Etusivu
    • Tietoa meistä
    • Palvelut
    • Tapaustutkimukset
    • Tiedä miten
    • Työurat
    • Sanakirja

      Palvelut

    • Se neuvoa-antava
    • Ohjelmistokehitys
    • Backend-kehitys
    • Frontend-kehitys
    • Staff Augmentation
    • Backend-kehittäjät
    • Pilvi-insinöörit
    • Tietoinsinöörit
    • Muut
    • QA insinöörit

      Resurssit

    • Faktoja ja myyttejä yhteistyöstä ulkoisen ohjelmistokehityskumppanin kanssa
    • Yhdysvalloista Eurooppaan: Miksi amerikkalaiset startup-yritykset päättävät muuttaa Eurooppaan?
    • Tech Offshore -kehityskeskusten vertailu: Tech Offshore Eurooppa (Puola), ASEAN (Filippiinit), Euraasia (Turkki).
    • Mitkä ovat teknologiajohtajien ja tietohallintojohtajien tärkeimmät haasteet?
    • Codest
    • Codest
    • Codest
    • Privacy policy
    • Verkkosivuston käyttöehdot

    Tekijänoikeus © 2025 by The Codest. Kaikki oikeudet pidätetään.

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