Após passar um tempo aprendendo e tendo uma ótima experiência com o GraphQL combinado com Typescript, percebi que não havia muito conteúdo sobre o tópico, principalmente em Português. Então decidi compartilhar um pouco do que tenho feito e aprendido com esse Framework incrível.
Sem mais delongas,
Bora codar!
Requerimentos
- Uma pequena introdução ao GraphQL
- Algumas linha de código com Typescript
O que vamos construir?
- Uma API de receitas
Antes de tudo, você vai precisar do Node.js instalado.
Acesse nodejs.org e faça o download da ultima versão estável.
Só mais umas palavrinhas antes de começarmos
GraphQL para recém-chegados
Feito para API, GraphQL tem um enorme poder, que nos permite consultar, inserir e manipular dados em nossa API , de uma maneira fácil e muito performática.
Não apenas isso mas, GraphQL também nos permite a receber apenas aquilo que desejamos. Sim, esqueça daquelas respostas gigantescas e cheia de informações desnecessárias em sua busca. Sua resposta terá apenas o que você requisitou, o que nos leva a um melhor gerenciamento de nossa API.
Sugiro que dê uma olhada em:
Por que TypeGraphQL?
Porque ele nos traz uma vida no desenvolvimento de nossa API, muito mais prazerosa e fácil de gerenciar, nos dando a habilidade de construir Schemas a partir de Classes e utilizando Decorators, e claro, tudo excelentemente tipado 🙂
Isso fará mais sentido assim que começarmos.
BORA!
Primeiro de tudo, vamos inicializar um novo projeto. Você pode usar npm ou yarn.
yarn init -y
Assim que inicializado, vamos adicionar as belezinhas desse projeto
yarn add graphql type-graphql reflect-metadata class-validator apollo-server
E
yarn add typescript @types/node nodemon -D
Agora devemos criar o arquivo de configuração do Typescript
tsc --init
E com algumas linhas adicionais, ele deve ficar assim:
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"lib": ["es2018", "esnext.asynciterable"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"strictPropertyInitialization": false,
"outDir": "./build",
}
}
O próximo passo é criar o nosso index.ts, que será o arquivo de inicialização de nossa aplicação.
Vamos cria-lo dentro de uma pasta /src.
Obs. Vamos colocar todo nosso código dentro desta pasta.
src/index.ts
import "reflect-metadata";
Uma exigencia do type-graphql para que tudo funcione, é importar o "reflect-metadata" no topo de nosso arquivo de inicialização.
Agora vamos criar os scripts que vão nos ajudar a compilar e rodar a nossa API.
Em seu package.json, adicione esses scripts, que eu ja te conto o que eles fazem:
"scripts": {
"watch": "tsc -w",
"dev": "nodemon build/index.js"
},
Com o script "watch", vamos compilar o nosso projeto, que são arquivos Typescript, para uma pasta que definimos no arquivo tsconfig.json, além de outras opções também.
Note que usamos uma flag "-w", que irá continuamente ficar assistindo os arquivos por mudanças, e irá compliar novamente, se houver alguma.
Agora que compilamos os arquivos Typescript para Javascript, precisamos rodar o script "dev" para que tudo comece a funcionar.
Este script vai rodar o nosso arquivo principal, que agora é um arquivo Javascript, dentro de nossa pasta /build.
E essa é a nossa configuração inicial 😉
Só para começarmos, vamos criar um Hello World no estilo GraphQL.
Dentro de /src vamos criar uma pasta /Resolvers. Nela ficarão todos os nossos resolvers, que terão os métodos de nossos tipos Query e Mutations, que vão nos ajudar a manipular nossos dados.
Dentro da pasta /Resolvers, criamos o arquivo helloResolver.ts
src/Resolvers/helloResolver.ts
import { Query, Resolver } from "type-graphql";
@Resolver()
export class helloResolver {
@Query(() => String)
hello() {
return "Hello!";
}
}
Usamos o @Resolver
Decorator para especificar um Resolver, e dentro dessa classe, nós adicionamos nossas funções, que seram nossas Queries ou Mutations, que por sua vez serão utilizadas para buscar ou enviar dados para nossa API.
Em nossa função hello()
dentro de nosso helloResolver
, nós estamos buscando dados, portanto, utilizamos o tipo Query.
Podemos até comparar a Query com nosso verbo GET, em APIs REST.
Já o tipo Mutation, pode ser comparado com os verbos POST, PUT, PATCH ou DELETE.
Agora vamos precisar dar um start em nossa aplicação.
Para isso vamos utilizar o Apollo Server, que é um servidor próprio para GraphQL e seus clients.
Sugiro que dê uma olhada no Apollo Server:
https://www.apollographql.com/docs/apollo-server/
Em nosso arquivo index.ts, vamos criar uma função bootstrap()
que inicializará nosso app.
src/index.ts
import "reflect-metadata";
import { ApolloServer } from "apollo-server";
import { buildSchema } from "type-graphql";
import { helloResolver } from "./Resolvers/helloResolver";
async function bootstrap() {
const server = new ApolloServer({
schema: await buildSchema({
resolvers: [helloResolver],
}),
});
await server.listen(4000, () => {
console.log("Apollo Server running at port: 4000");
});
}
bootstrap().catch((error) => console.log(error));
Dentro dela, vamos criar uma instância do Apollo Server. Como uma dependência, vamos criar um Schema, que vai receber os nossos Resolvers, que no caso, acabamos de criar o primeiro
Por último vamos chamar a função listen()
do nosso server, passando a porta da aplicação e uma função callback, dizendo que o app está rodando. Se tudo ocorreu bem, claro 🤞🏼
Bora testar!
Rode os scripts
yarn watch
E
yarn dev
Dê uma olhada em http://localhost:4000
Tudo rodando! 🏃🏼
Agora vamos testar nossa primeira Query.
Incrível! 🔥
Agora que já está tudo funcionando corretamente, vamos para o nosso projeto!
Primeiro de tudo, vamos criar a nossa Entidade Recipe, que será nossa Model para o tipo Recipe.
Dentro de uma pasta /Entities, criamos a nossa entidade Recipe.
src/Entities/Recipe.ts
export class Recipe {
id: string;
name: string;
description: string;
ingredients: Array<string>;
}
Essa é nossa classe Recipe.
Agora com o Decorator @ObjectType
, criamos nosso "Tipo Objeto", para o type-graphql defini-lo e utiliza-lo como um Schema.
E também utilizamos o Decorator @Field
para marcar as propriedades de nosso objeto.
import { Field, ObjectType } from "type-graphql";
@ObjectType()
export class Recipe {
@Field()
id: string;
@Field()
name: string;
@Field({ nullable: true })
description: string;
@Field(() => [String])
ingredients: Array<string>;
}
Esses campos podem receber opções, como por exemplo em description, a opção nullable que define que nossa propriedade pode ser nula.
O Decorator irá auto-inferir os tipos String ou Boolean, mas há uma limitação para tipos genéricos, como Array ou Promises, para esses nos devemos "inferir manualmente", como foi feito no campo ingredients.
Antes de criar nosso Recipe Resolver, vamos criar o nosso banco de dados, para armazenarmos nossas receitas.
Para facilitar, não vamos utilizar nenhum banco como MongoDB ou mesmo PostgreSQL. Nosso banco de dados será uma simples Array de Recipe - Isso é para facilitar o tutorial, principalmente para quem está começando 🤓.
Sendo assim, vamos criar nosso incrível banco de dados.
Criamos um arquivo data.ts dentro de nossa pasta /src
import { Recipe } from "./Entities/Recipe";
export const recipeData: Array<Recipe> = [];
Apenas exportamos uma Array de Recipe.
E é isso para nosso banco.
Se esse material tiver uma boa relevância para vocês devs, posso fazer algum outro post/tutorial abordando uma API com a implementação de algum banco de dados um pouco mais interessante do que a nossa humilde recipeData 😂
Enfim, vamos começar com nosso Recipe Resolver.
Dentro da pasta /Resolvers, vamos criar nosso recipeResolver.ts
/src/Resolvers/recipeResolver.ts
import { Query, Resolver } from "type-graphql";
import { recipeData } from "../recipeData";
@Resolver()
export class recipeResolver {
@Query(() => [Recipe])
getRecipes() {
return recipeData;
}
}
Assim como o nosso Hello World Resolver, vamos criar a classe recipeResolver.
Nela começamos com nosso primeiro tipo Query, para buscar os dados que temos de receitas.
Assim que salvarmos, e enviarmos a Query, teremos o seguinte resultado:
Vazio como esperado. Agora vamos criar uma Mutation para inserirmos novas receitas e assim nossa Query getRecipes()
poderá mostra-los.
Como já sabemos, para criar nossa receita, vamos precisar de uma Mutation
Além disso, também vamos precisar enviar o objeto do tipo Recipe, com as informações de nossa receita.
Vamos definir este objeto como data com o @Arg()
Decorator.
import { Query, Mutation, Resolver, Arg } from "type-graphql";
import { recipeData } from "../recipeData";
@Resolver()
export class recipeResolver {
@Mutation(() => Recipe)
createRecipe(@Arg("data") data: Recipe) { }
}
Agora quando formos enviar nossa Mutation, vamos passar nosso objeto data, para assim conseguirmos armazená-lo em nosso banco.
Mas há um porém. O nosso tipo Recipe possui uma propriedade id, mas no caso não vamos deixar o cliente enviar esta informação, vamos criá-la em nosso método createRecipe()
Para nos auxiliar, vamos utilizar uma biblioteca chamada uuidv4
Para instalar essa lib, podemos utilizar nosso gerenciador de pacotes favorito:
yarn add uuidv4
Logo, nossa createRecipe()
Mutation vai ficar assim:
import { Query, Mutation, Resolver, Arg } from "type-graphql";
import { v4 as uuidv4 } from "uuid";
import { recipeData } from "../recipeData";
@Resolver()
export class recipeResolver {
@Mutation(() => Recipe)
createRecipe(@Arg("data") data: Recipe) {
let newRecipe = { ...data, id: uuidv4() };
recipeData.push(newRecipe);
return newRecipe;
}
}
Não sei se percebeu, mas ainda tem uma coisinha que precisamos mudar...
Nosso argumento data, ainda está com o tipo Recipe. 🤔
Vamos resolver isso criando um tipo para data
Criamos uma pasta /Types e nela criamos o arquivo RecipeDataType.ts
src/Types/RecipeDataType.ts
import { Field, InputType } from "type-graphql";
@InputType()
export class RecipeDataType {
@Field()
name: string;
@Field()
description: string;
@Field(() => [String])
ingredients: Array<string>;
}
Diferente de nosso Tipo Objeto, para esse vamos utilizar o Tipo Input @InputType
Agora podemos adicionar esse tipo para nosso data
import { Query, Mutation, Resolver, Arg } from "type-graphql";
import { v4 as uuidv4 } from "uuid";
import { recipeData } from "../recipeData";
import { RecipeDataType } from "../Types/RecipeDataType";
@Resolver()
export class recipeResolver {
@Mutation(() => Recipe)
createRecipe(@Arg("data") data: RecipeDataType) {
let newRecipe = { ...data, id: uuidv4() };
recipeData.push(newRecipe);
return newRecipe;
}
}
Hora de testar!
Irado! 🔥
Para nossas Mutations de delete e update, vamos fazer algo muito parecido, apenas mudando o jeito que manipulamos os dados em nosso banco.
A Mutation deleteRecipe()
, vai precisar de um argumento id, para assim, conseguirmos buscar a receita a ser deletada dentro de nosso banco de dados.
import { Query, Mutation, Resolver, Arg } from "type-graphql";
import { recipeData } from "../recipeData";
import { RecipeDataType } from "../Types/RecipeDataType";
import { v4 as uuidv4 } from "uuid";
@Resolver()
export class recipeResolver {
@Mutation(() => Boolean)
deleteRecipe(@Arg("id") id: string): Boolean {
let recipeIndex = recipeData.findIndex((recipe) => recipe.id === id);
if (recipeIndex > -1) {
recipeData.splice(recipeIndex, 1);
return true;
}
return false;
}
}
Vamos dar uma olhada no resultado
Só não vamos esquecer de criar uma nova receita para podermos deletá-la. Isso acontece porque o nosso "banco de dados" está na memória, e sempre que a aplicação recarregar, vamos perder as nossas deliciosas receitas 😢 Mas não tem problema por agora 😊
Agora com nosso id, vamos usá-lo para deletar a receita.
Brabo!! estamos quase no final 😎
Por ultimo, nossa Mutation updateRecipe()
Para essa, vamos utilizar 2 argumentos, o id da receita para ser alterada, e também o data, que vai conter as novas informações da receita.
import { Query, Mutation, Resolver } from "type-graphql";
import { recipeData } from "../recipeData";
import { RecipeDataType } from "../Types/RecipeDataType";
import { v4 as uuidv4 } from "uuid";
@Resolver()
export class recipeResolver {
@Mutation(() => Recipe)
updateRecipe(
@Arg("id") id: string,
@Arg("data") data: RecipeDataType
) {
let recipeIndex = recipeData.findIndex((recipe) => recipe.id === id);
if (recipeIndex > -1) {
recipeData[recipeIndex] = { ...data, id };
return recipeData[recipeIndex];
}
}
}
Checando...
Ta lá! nossa API de receitas. 😎
Agora podemos dar apenas um toque final...
Como você deve ter percebido, nós não especificamos nenhum tipo de respostas para as nossas Queries e Mutations, e isso está um pouco feio...
Vamos criar!
Para esse tipo, vamos utilizar o Decorator @ObjectType()
Dentro de /Types, vamos criar ResponseType.ts
src/Types/ResponseType.ts
import { Field, ObjectType } from "type-graphql";
import { Recipe } from "../Entities/Recipe";
@ObjectType()
export class ErrorType {
@Field()
message: string;
}
@ObjectType()
export class ResponseType {
@Field(() => Recipe, { nullable: true })
recipe?: Recipe;
@Field(() => ErrorType, { nullable: true })
error?: ErrorType;
}
Agora precisamos apenas adicionar esse tipo como resposta de nossos métodos em nosso Resolver.
Nosso recipeResolver.ts completo, ficará assim:
import { Arg, Mutation, Query, Resolver } from "type-graphql";
import { v4 as uuidv4 } from "uuid";
import { Recipe } from "../Entities/Recipe";
import { recipeData } from "../recipeData";
import { RecipeDataType } from "../Types/RecipeDataType";
import { ResponseType } from "../Types/ResponseType";
@Resolver()
export class recipeResolver {
@Query(() => [Recipe])
recipes() {
return recipeData;
}
@Query(() => ResponseType)
getRecipe(@Arg("id") id: string): ResponseType {
let recipe = recipeData.find((recipe) => recipe.id === id);
if (recipe) {
return { recipe };
}
return {
error: {
message: "Recipe not found",
},
};
}
@Mutation(() => ResponseType)
createRecipe(@Arg("data") data: RecipeDataType): ResponseType {
let newRecipe: Recipe = { ...data, id: uuidv4() };
recipeData.push(newRecipe);
if (newRecipe) {
return {
recipe: newRecipe,
};
}
return {
error: {
message: "Could not create your recipe",
},
};
}
@Mutation(() => ResponseType)
deleteRecipe(@Arg("id") id: string): ResponseType {
let recipeIndex = recipeData.findIndex((recipe) => recipe.id === id);
if (recipeIndex > -1) {
recipeData.splice(recipeIndex, 1);
return {};
}
return {
error: {
message: "Recipe not found",
},
};
}
@Mutation(() => ResponseType)
updateRecipe(
@Arg("id") id: string,
@Arg("data") data: RecipeDataType
): ResponseType {
let recipeIndex = recipeData.findIndex((recipe) => recipe.id === id);
if (recipeIndex > -1) {
recipeData[recipeIndex] = { ...data, id };
return { recipe: recipeData[recipeIndex] };
}
return {
error: {
message: "Recipe not found",
},
};
}
}
Vamos dar uma olhada final
DEMAIS! 🔥
Agora está melhor, mas ainda sim, há espaço para MUITA melhoria. Mas agora é contigo.
Nesse ponto você provavelmente vai estar com o básico de conhecimento de TypeGraphQL, e talvez, assim como eu, percebeu os benefícios de codar sua API GrahpQL com Typescript.
Se tiver afim, faça um fork desse projeto ou mesmo crie um PR com suas alterações e considerações, vou curtir demais trocar conhecimento 👍🏼
Espero que esse post tenha sido útil. Obrigado.
Código fonte:
Happy Coding! 🙂
Top comments (0)