DEV Community

Cover image for TypeGraphQL, o básico em uma API de receitas
Vinicius Savegnago
Vinicius Savegnago

Posted on • Edited on

TypeGraphQL, o básico em uma API de receitas

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.

GraphQL

Sugiro que dê uma olhada em:

https://graphql.org

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
Enter fullscreen mode Exit fullscreen mode

 

Assim que inicializado, vamos adicionar as belezinhas desse projeto

yarn add graphql type-graphql reflect-metadata class-validator apollo-server
Enter fullscreen mode Exit fullscreen mode

E

yarn add typescript @types/node nodemon -D
Enter fullscreen mode Exit fullscreen mode

 

Agora devemos criar o arquivo de configuração do Typescript

tsc --init
Enter fullscreen mode Exit fullscreen mode

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",
  }
}
Enter fullscreen mode Exit fullscreen mode

 

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";
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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!";
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

E

yarn dev
Enter fullscreen mode Exit fullscreen mode

Dê uma olhada em http://localhost:4000

Tudo rodando! 🏃🏼

 

Agora vamos testar nossa primeira Query.

Alt Text

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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> = [];
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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

}
Enter fullscreen mode Exit fullscreen mode

 

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
Enter fullscreen mode Exit fullscreen mode

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;      

  }

}
Enter fullscreen mode Exit fullscreen mode

 

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>;
}
Enter fullscreen mode Exit fullscreen mode

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;      

  }

}
Enter fullscreen mode Exit fullscreen mode

 

Hora de testar!

Alt Text

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

}
Enter fullscreen mode Exit fullscreen mode

 

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.

Alt Text

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

  }

}
Enter fullscreen mode Exit fullscreen mode

Checando...

Alt Text

 

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;
}
Enter fullscreen mode Exit fullscreen mode

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",
      },
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

 

Vamos dar uma olhada final

Alt Text

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:

vinisaveg/typegraphql-basics

 

Happy Coding! 🙂

Top comments (0)