GraphQL Apollo server su TypeScript

Kodėl verta rinktis Apollo Server su TypeScript

Jei kada nors bandėte kurti REST API su daugybe endpoint’ų, tikriausiai žinote tą skausmą, kai reikia palaikyti versijas, rašyti dokumentaciją ir nuolat aiškintis su frontend komanda, kokių duomenų jiems tiksliai reikia. GraphQL atėjo kaip gelbėtojas, o Apollo Server tapo vienu populiariausių sprendimų šiai technologijai implementuoti. Kai dar pridedi TypeScript į šią lygtį, gauni tikrą developer experience svajonę.

Apollo Server nėra vienintelis GraphQL serverio sprendimas, bet jis tikrai yra vienas brandžiausių ir geriausiai dokumentuotų. Jis puikiai integruojasi su Express, Fastify, Koa ir kitais populiariais Node.js framework’ais. O TypeScript? Na, jis tiesiog užtikrina, kad jūsų kodas nevirsta chaotiška spagetų krūva, kai projektas auga.

Realybėje daugelis komandų pradeda su JavaScript, bet vėliau pereina prie TypeScript, kai projektas tampa sudėtingesnis. Geriau iš karto pradėti su TypeScript – sutaupysite daug nervų vėliau.

Projekto paruošimas ir priklausomybės

Pradėkime nuo pradžių. Pirmiausia reikia sukurti projektą ir įdiegti reikalingas priklausomybes. Čia nėra jokios magijos, bet yra keletas niuansų, kuriuos verta žinoti.


npm init -y
npm install @apollo/server graphql
npm install -D typescript @types/node ts-node nodemon

Dabar sukurkite tsconfig.json failą. Galite naudoti npx tsc --init, bet aš rekomenduoju tokią konfigūraciją, kuri tikrai veikia gerai su Apollo Server:


{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

Viena svarbi detalė – įsitikinkite, kad strict režimas yra įjungtas. Taip, jis kartais erzina su savo klaidomis, bet būtent dėl to mes ir naudojame TypeScript. Jei norite lengvo gyvenimo be type safety, galite likti su JavaScript.

Pirmasis veikiantis Apollo Server

Sukurkime paprasčiausią veikiantį serverį. Sukurkite src/index.ts failą:


import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = `#graphql
type Book {
title: String
author: String
}

type Query {
books: [Book]
}
`;

const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];

const resolvers = {
Query: {
books: () => books,
},
};

async function startServer() {
const server = new ApolloServer({
typeDefs,
resolvers,
});

const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});

console.log(`🚀 Server ready at: ${url}`);
}

startServer();

Paleiskite su npx ts-node src/index.ts ir eikite į http://localhost:4000. Pamatysite Apollo Studio Explorer – interaktyvią aplinką, kur galite testuoti savo queries. Tai nepalyginamai geriau nei Postman REST API testavimui.

TypeScript tipų integracija su GraphQL schema

Dabar pats įdomiausias dalykas. Turime GraphQL schemą, bet TypeScript apie ją nieko nežino. Galime rašyti tipus rankiniu būdu, bet tai būtų absurdas. Čia į pagalbą ateina graphql-codegen.


npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers

Sukurkite codegen.yml failą projekto šakniniame kataloge:


schema: "./src/schema.graphql"
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
config:
useIndexSignature: true
contextType: "../context#Context"

Dabar perkelkime mūsų schema į atskirą failą src/schema.graphql:


type Book {
title: String!
author: String!
publishedYear: Int
}

type Query {
books: [Book!]!
book(id: ID!): Book
}

type Mutation {
addBook(title: String!, author: String!, publishedYear: Int): Book!
}

Paleiskite npx graphql-codegen ir jūs gausite sugeneruotus tipus src/generated/graphql.ts faile. Dabar jūsų resolveriai gali būti pilnai type-safe.

Resolverių implementacija su tipais

Dabar parodysiu, kaip atrodo tikri, production-ready resolveriai su TypeScript:


import { Resolvers, Book } from './generated/graphql';

interface BookModel {
id: string;
title: string;
author: string;
publishedYear?: number;
}

const booksDatabase: BookModel[] = [
{ id: '1', title: 'The Awakening', author: 'Kate Chopin', publishedYear: 1899 },
{ id: '2', title: 'City of Glass', author: 'Paul Auster', publishedYear: 1985 },
];

export const resolvers: Resolvers = {
Query: {
books: (): BookModel[] => {
return booksDatabase;
},
book: (_, { id }): BookModel | undefined => {
return booksDatabase.find(book => book.id === id);
},
},
Mutation: {
addBook: (_, { title, author, publishedYear }): BookModel => {
const newBook: BookModel = {
id: String(booksDatabase.length + 1),
title,
author,
publishedYear: publishedYear ?? undefined,
};
booksDatabase.push(newBook);
return newBook;
},
},
};

Pastebėkite, kaip TypeScript dabar tikrina, ar jūsų resolveriai tikrai grąžina tai, ką žada schema. Jei pamirštumėte grąžinti kokį nors lauką arba grąžintumėte neteisingą tipą, gautumėte klaidą dar prieš paleidžiant serverį.

Context ir autentifikacija

Realaus pasaulio aplikacijose jums reikės context objekto, kuris perduodamas visiems resolveriams. Čia paprastai saugoma informacija apie prisijungusį vartotoją, duomenų bazės connection’ai ir panašūs dalykai.

Sukurkime src/context.ts:


import { Request } from 'express';

export interface Context {
user?: {
id: string;
email: string;
role: string;
};
dataSources: {
// čia būtų jūsų data sources
};
}

export async function createContext({ req }: { req: Request }): Promise {
const token = req.headers.authorization || '';

// Čia turėtų būti tikras token'o validavimas
const user = await validateToken(token);

return {
user,
dataSources: {
// inicializuokite data sources
},
};
}

async function validateToken(token: string) {
// Supaprastintas pavyzdys
if (!token) return undefined;

// Realybėje čia būtų JWT validacija ar panašiai
return {
id: '1',
email: '[email protected]',
role: 'USER',
};
}

Dabar atnaujinkime serverio konfigūraciją:


import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
import cors from 'cors';
import { json } from 'body-parser';
import { createContext, Context } from './context';
import { resolvers } from './resolvers';
import { typeDefs } from './schema';

async function startServer() {
const app = express();

const server = new ApolloServer({
typeDefs,
resolvers,
});

await server.start();

app.use(
'/graphql',
cors(),
json(),
expressMiddleware(server, {
context: createContext,
})
);

app.listen(4000, () => {
console.log(`🚀 Server ready at http://localhost:4000/graphql`);
});
}

startServer();

Error handling ir validacija

Vienas dalykas, kurį daugelis pradedančiųjų praleidžia – tinkamas klaidų valdymas. GraphQL turi savo klaidų sistemą, ir Apollo Server leidžia ją išplėsti.

Sukurkime custom error klases:


import { GraphQLError } from 'graphql';

export class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED',
http: { status: 401 },
},
});
}
}

export class ForbiddenError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'FORBIDDEN',
http: { status: 403 },
},
});
}
}

export class ValidationError extends GraphQLError {
constructor(message: string, field?: string) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT',
field,
http: { status: 400 },
},
});
}
}

Dabar galime naudoti šias klaidas resolveriuose:


import { Resolvers } from './generated/graphql';
import { AuthenticationError, ValidationError } from './errors';

export const resolvers: Resolvers = {
Mutation: {
addBook: (_, { title, author }, context) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in to add books');
}

if (title.length < 3) { throw new ValidationError('Title must be at least 3 characters', 'title'); } // Toliau eina normali logika const newBook = { id: String(Date.now()), title, author, }; return newBook; }, }, };

Kaip tai viskas veikia production aplinkoje

Gerai, turime veikiantį serverį su TypeScript, bet kaip jį paleisti production'e? Pirmiausia reikia sukompiliuoti TypeScript į JavaScript.

Atnaujinkite package.json scripts sekciją:


"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"codegen": "graphql-codegen",
"type-check": "tsc --noEmit"
}

Production aplinkoje turėtumėte išjungti Apollo Studio (playground). Tai daroma taip:


const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
plugins: [
ApolloServerPluginLandingPageLocalDefault({
embed: false
}),
],
});

Dar vienas svarbus dalykas – rate limiting. Apollo Server pats to nedaro, bet galite naudoti graphql-rate-limit arba implementuoti savo sprendimą per middleware.

Štai paprastas rate limiting pavyzdys:


import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minučių
max: 100, // maksimaliai 100 request'ų per window
message: 'Too many requests from this IP',
});

app.use('/graphql', limiter);

Dėl performance, rekomenduoju naudoti DataLoader pattern'ą, kad išvengtumėte N+1 query problemos. Tai ypač aktualu, kai turite nested relationships schemoje. Apollo Server puikiai veikia su DataLoader, tik reikia jį pridėti į context.

Monitoring'ui galite naudoti Apollo Studio arba integruoti su Sentry, Datadog ar kitu monitoring įrankiu. Apollo Server turi plugin sistemą, kuri leidžia lengvai pridėti custom logging'ą ar metrics.

Taip pat nepamirškite, kad GraphQL queries gali būti labai sunkūs serveriui, jei klientas paprašo per daug nested duomenų. Rekomenduoju naudoti graphql-depth-limit ir graphql-validation-complexity bibliotekus, kad apsaugotumėte serverį nuo per sudėtingų query.

Realybėje Apollo Server su TypeScript yra galingas combo, kuris leidžia kurti patikimus, lengvai prižiūrimus API. Pradžioje gali atrodyti, kad setup'o yra daug, bet kai projektas auga, būsite dėkingi už visą tą type safety ir developer experience, kurį gausite. Svarbiausia – nepersistenkite iš karto su optimizacijomis, pradėkite paprastai ir plėskite pagal poreikį.

Daugiau

Apache Pinot: OLAP database