GraphQL ou REST: descubra a melhor escolha para seu App Flutter

GraphQL ou REST: descubra a melhor escolha para seu App Flutter

E aí, pessoa exploradora do Flutter! Tudo certo por aí?

Você já criou alguma aplicação que faz comunicação com APIs?

Sim? Então, deixa eu te fazer uma outra pergunta: você já trabalhou com APIs REST ou GraphQL? Se a resposta for sim, ótimo!

Você já deu o primeiro passo e esse artigo vai te ajudar a tomar as próximas decisões. Se a resposta for não, não se preocupe, você está no lugar certo!

Neste artigo, vamos explicar o que são essas APIs, falar sobre as vantagens e limitações de cada uma e, claro, te ajudar a escolher qual delas é a melhor para o seu projeto.

Vamos nessa?

O que é REST?

Uma API REST (Representational State Transfer) é uma forma de comunicação com um servidor, utilizando requisições HTTP. Essas requisições podem ser de quatro tipos principais: GET, POST, PUT e DELETE.

Se você está começando agora e dando os primeiros passos na comunicação com APIs, pode ser que ainda se sinta um pouco perdido com essas operações,mas não se preocupe! vamos entender todas elas na prática.

Antes de começarmos a fazer as requisições, precisamos criar nossa própria API REST. Para isso, vamos usar o json-server.

Como criar sua API Rest

Primeiro, será necessário que você tenha o Node instalado no seu computador, para fazer isso, você pode acessar esse link: Como instalar o Node.js no Windows, Linux e macOS.

Após a instalação, no diretório do seu projeto Flutter, vá até a pasta lib e crie uma nova pasta chamada api.

Dentro da pasta api, crie um arquivo chamado api.json e adicione o seguinte conteúdo:

{
  "users": [
    {
      "id": 1,
      "name": "João Silva",
      "email": "[email protected]"
    },
    {
      "id": 2,
      "name": "Maria Oliveira",
      "email": "[email protected]"
    },
    {
      "id": 3,
      "name": "Carlos Pereira",
      "email": "[email protected]"
    },
    {
      "id": 4,
      "name": "Ana Costa",
      "email": "[email protected]"
    }
  ]
}

Em seguida, abra o terminal dentro da pasta api e execute o comando:

npx json-server api.json

Pronto! Sua API REST já está em funcionamento, para acessá-la, basta abrir o navegador e digitar a URL: "localhost:3000/users".

Incrível, não é? Nossa API já está configurada e, agora, podemos começar a realizar operações básicas. Vamos iniciar com o GET.

GET

Vamos supor que nossa API é um álbum de fotos, cada endpoint é uma página que o álbum possui, e ao fazer uma requisição GET, você escolhe uma página (um endpoint) e recebe as fotos(os dados) que ele oferece.

Na nossa aplicação, quando fazemos uma requisição GET no endpoint users, estamos pedindo à nossa API uma lista de todas as pessoas usuárias cadastradas.

A seguir temos um exemplo de como implementar o GET:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> getUsers() async {
  final response = await http.get(Uri.parse('http://10.0.2.2:3000/users'));

  if (response.statusCode == 200) {
    List<dynamic> users = json.decode(response.body);
    print(users);
  } else {
    print('Falha ao carregar dados');
  }
}

Neste trecho de código, a requisição GET busca todos os dados do endpoint \users.

Se tudo der certo (status 200), o código converte a resposta, que vem em formato JSON, para uma lista e imprime os dados.

Agora que sabemos como buscar dados, vamos aprender como inserir informações na API usando o POST.

POST

Voltando à analogia do álbum de fotos: ao fazer um POST, você está adicionando uma foto em alguma página do álbum (ou seja, inserindo dados no endpoint).

Analise o exemplo a seguir:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> createUser() async {
  final response = await http.post(
    Uri.parse('http://10.0.2.2:3000/users'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({
      'name': 'Nova pessoa usuária',
      'email': '[email protected]'
    }),
  );

  if (response.statusCode == 201) {
    print('Pessoa usuária criado');
  } else {
    print('Falha ao criar pessoa usuária');
  }
}

Neste exemplo, enviamos uma requisição POST para criar uma nova pessoa usuária. Os dados (nome e email) são enviados no formato JSON.

O POST é usado para enviar dados ao servidor, criando algo novo.

Certo, já aprendemos a buscar e criar dados. Agora, vamos aprender como atualizar as informações usando o PUT.

PUT

Ainda utilizando a analogias das fotos, com o método PUT, você substitui uma foto antiga por uma versão mais recente.

Vamos analisar uma implementação do método PUT:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> updateUser() async {
  final response = await http.put(
    Uri.parse('http://10.0.2.2:3000/users/1'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({
      'name': 'João Silva Atualizado',
      'email': '[email protected]'
    }),
  );

  if (response.statusCode == 200) {
    print('Atualização feita!');
  } else {
    print('Falha na atualização!');
  }
}

Fizemos uma requisição PUT para atualizar os dados de uma pessoa usuária com o id 1. Se a atualização for bem-sucedida (status 200), o código imprime uma mensagem confirmando a atualização.

O PUT é usado para atualizar um item que já existe. Ou seja, você altera as informações de algo que já está armazenado no servidor.

Mas ainda está faltando algo, não é? E se quisermos excluir algum dado? Para isso, usamos o DELETE!

DELETE

Ao fazer uma requisição DELETE, você tira uma foto do álbum. A seguir, temos um trecho de código que mostra o funcionamento do DELETE:

import 'package:http/http.dart' as http;

Future<void> deleteUser() async {
  final response = await http.delete(Uri.parse('http://10.0.2.2:3000/users/1'));

  if (response.statusCode == 200) {
    print('Pessoa usuária deletada!');
  } else {
    print('Falha ao deletar pessoa usuária!');
  }
}

Neste código, enviamos uma requisição DELETE para excluir o item com id 1 da nossa API. Se a exclusão for bem-sucedida (status 200), o código imprime uma mensagem confirmando que o dado foi deletado.

O DELETE é usado para excluir um item. Ou seja, você pode remover dados do servidor quando não precisar mais deles.

Pronto! Agora entendemos o funcionamento básico das operações de uma API REST. Elas nos permitem adicionar, alterar, consultar e excluir dados.

Mas, como "nem tudo são flores", apesar da praticidade, essas APIs possuem algumas limitações, e vamos entender elas a seguir.

Banner promocional da Alura, com um design futurista em tons de azul, apresentando o texto

Problemas das APIs REST

Apesar de serem muito populares e funcionais, as APIs REST não são perfeitas e, nós vamos entender as limitações que elas apresentam a seguir:

Front-end muito dependente do back-end

Com APIs REST, o front-end precisa saber exatamente o que o back-end vai devolver. Isso faz com que o front-end fique "refém" do que o back-end oferece através dos seus endpoints.

Por exemplo, na nossa API com endpoint users, se quiséssemos apenas os nomes das pessoas que estão cadastrados, nós teríamos um endpoint que devolve apenas isso? Não!

Uma das alternativas para resolver esse problema seria solicitar ao back-end disponibilizar uma rota de nomes das pessoas cadastradas, nosso arquivo api.json ficaria assim:

{
    "users": [
        {
            "id": 1,
            "name": "João Silva",
            "email": "[email protected]"
        },
        {
            "id": 2,
            "name": "Maria Oliveira",
            "email": "[email protected]"
        },
        {
            "id": 3,
            "name": "Carlos Pereira",
            "email": "[email protected]"
        },
        {
            "id": 4,
            "name": "Ana Costa",
            "email": "[email protected]"
        }
    ],
    "names": [
        "João Silva",
        "Maria Oliveira",
        "Carlos Pereira",
        "Ana Costa"
    ]
}

Mas essa solução cria um grande problema de escalabilidade: a cada novo tipo de dado que você quer retornar, seria necessário criar um novo endpoint. E isso pode gerar um número grande de endpoints, tornando a manutenção da API mais difícil.

Talvez você já tenha pensado em uma outra solução, que é basicamente buscar as pessoas usuárias e filtrar apenas os nomes delas. Mas já posso adiantar que essa estratégia também tem um problema e vamos entendê-lo a seguir.

Overfetching

Uma solução para que consigamos apenas os nomes das pessoas cadastradas seria utilizarmos um GET no endpoint users, receber os dados da API e filtrar apenas os nomes. O código ficaria assim:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> getUsers() async {
  final response = await http.get(Uri.parse('http://10.0.2.2:3000/users'));

  if (response.statusCode == 200) {
    List<dynamic> users = json.decode(response.body);

    List<String> userNames = users.map((user) => user['name'].toString()).toList();

    print('Nomes das pessoas:');
    userNames.forEach((name) => print(name));
  } else {
    print('Falha ao carregar dados');
  }
}

Mas qual o problema de usarmos essa estratégia? Estamos desperdiçando memória e banda da pessoa usuária.

Como no nosso álbum só conseguimos acessar as fotos pelas páginas disponíveis, se você quiser ver apenas algumas, vai precisar navegar pela página inteira e escolher as que mais te interessam aos poucos.

Da mesma forma, como nosso endpoint retorna todos os dados das pessoas cadastradas, é necessário fazer o consumo completo desses dados e depois descartar o que não precisamos. Isso é o que chamamos de overfetching.

Agora que entendemos o overfetching, vamos falar sobre o problema oposto: o underfetching.

Underfetching

Vamos adicionar uma nova funcionalidade para nossa API: um endpoint de posts, onde cada pessoa tem seus próprios posts.

Algo desse tipo:

{
  "users": [
    {
      "id": 1,
      "nome": "João Silva",
      "email": "[email protected]"
    },
    {
      "id": 2,
      "nome": "Maria Oliveira",
      "email": "[email protected]"
    },
    {
      "id": 3,
      "nome": "Carlos Pereira",
      "email": "[email protected]"
    },
    {
      "id": 4,
      "nome": "Ana Costa",
      "email": "[email protected]"
    }
  ],
  "posts": [
    {
      "id": 1,
      "userId": 1,
      "description": "João Silva está aproveitando o dia com muito aprendizado!"
    },
    {
      "id": 2,
      "userId": 2,
      "description": "Maria Oliveira, sua energia positiva sempre inspira a todos ao seu redor!"
    },
    {
      "id": 3,
      "userId": 3,
      "description": "Carlos Pereira está pronto para novos desafios!"
    },
    {
      "id": 4,
      "userId": 4,
      "description": "Ana Costa, cada dia é uma nova oportunidade para brilhar!"
    },
    {
      "id": 5,
      "userId": 1,
      "description": "Hoje é um ótimo dia para aprender algo novo, não é, pessoal?"
    }
  ]
}

Se você precisasse acessar os posts de alguém junto com o nome e e-mail da pessoa, você teria que fazer múltiplas requisições: uma para o endpoint de users e outra para o de posts.

Usando a analogia do álbum de fotos: se você quer achar fotos que estão em páginas diferentes, vai precisar abrir cada página e pegar a foto que está buscando.

No nosso código, se precisássemos buscar os posts de um pessoa junto com os dados dela, poderíamos ter algo assim:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> getUserWithPosts(int userId) async {
  final userResponse = await http.get(Uri.parse('http://10.0.2.2:3000/users/$userId'));

  final postsResponse = await http.get(Uri.parse('http://10.0.2.2:3000/posts?userId=$userId'));

  if (userResponse.statusCode == 200 && postsResponse.statusCode == 200) {
    var user = json.decode(userResponse.body);
    var posts = json.decode(postsResponse.body);

    print('Pessoa: ${user['name']}');
    print('Posts: $posts');
  } else {
    print('Falha ao carregar dados');
  }
}

Neste exemplo, estamos fazendo duas requisições HTTP separadas: uma para buscar a pessoa usuária e outra para buscar os posts dessa pessoa usuária.

Isso gera underfetching, porque estamos fazendo requisições separadas e, em vez de obter todos os dados necessários de uma só vez, estamos dividindo-os em várias etapas, o que aumenta a latência e a quantidade de solicitações ao servidor.

Para resolver esse gargalo o ideal seria ter um endpoint que já trouxesse todos os dados que você precisa, como o nome, o e-mail e os posts de uma vez só. Ou seja, novamente estamos dependentes do nosso back-end!

Agora que entendemos as limitações do REST, vem a pergunta: "Será que existe uma alternativa às APIs REST?".

A resposta é sim! E aqui entra o GraphQL.

O que é GraphQL?

Com o GraphQL você não tem o trabalho de buscar em várias páginas a sua foto, você apenas informa quais fotos quer e elas lhe serão retornadas.

GraphQL é uma linguagem de consulta para APIs criada pelo Facebook. Diferente do REST, onde você recebe um pacote fechado de informações, no GraphQL você escolhe exatamente o que quer buscar.

Criando sua API GraphQL

Antes de conectar uma aplicação Flutter a uma API GraphQL, vamos criar um servidor básico. Primeiro, crie um diretório para o projeto e inicialize o Node.js com o comando:

mkdir graphql-server
cd graphql-server
npm init -y

Depois, instale as dependências necessárias:

npm install apollo-server graphql

Agora, crie um arquivo chamado index.js e adicione o seguinte código:

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type User {
    id: ID
    nome: String
    email: String
  }

  type Post {
    id: ID
    userID: Int
    descricao: String
  }

  type Query {
    users: [User]
    posts(userID: Int): [Post]  # Adicionando um argumento 'userID' para a query de posts
  }

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

const users = [
  { id: "1", nome: "João Silva", email: "[email protected]" },
  { id: "2", nome: "Maria Oliveira", email: "[email protected]" },
];

const posts = [
  { id: "1", userID: 1, descricao: "Post de João Silva" },
  { id: "2", userID: 2, descricao: "Post de Maria Oliveira" },
];

const resolvers = {
  Query: {
    users: () => users,
    posts: (_, { userID }) => {
      if (userID) {
        return posts.filter(post => post.userID === userID);
      }
      return posts; 
    },
  },
  Mutation: {
    createUser: (_, { nome, email }) => {
      const newUser = { id: `${users.length + 1}`, nome, email };
      users.push(newUser);
      return newUser;
    },
  },
};

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

server.listen().then(({ url }) => {
  console.log(`Servidor GraphQL rodando em ${url}`);
});

Por fim, inicie o servidor com:

node index.js

A nossa API GraphQL já está no ar e você pode acessar ela pelo link http://localhost:4000. Só colar esse endereço no seu navegador e pronto!

Você será redirecionado para o GraphQL Playground, onde pode testar as operações à vontade.

Aqui no artigo, vamos fazer tudo no Flutter, mas se preferir, pode usar o Playground também. Fique à vontade!

Agora, indo para o Flutter, utilizaremos o pacote graphql_flutter para consumir essa API. Adicione a dependência no arquivo pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  graphql_flutter:

Em seguida, instale as dependências:

flutter pub get

Para conectar a API ao Flutter, configure o GraphQLClient no código principal:

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

void main() {
  final HttpLink httpLink = HttpLink('http://10.0.2.2:4000');

  final GraphQLClient client = GraphQLClient(
    link: httpLink,
    cache: GraphQLCache(),
  );

  runApp(MyApp(client: client));
}

class MyApp extends StatelessWidget {
  final GraphQLClient client;

  MyApp({required this.client});

  @override
  Widget build(BuildContext context) {
    final ValueNotifier<GraphQLClient> clientNotifier = ValueNotifier(client);

    return GraphQLProvider(
      client: clientNotifier,
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}

Agora podemos explorar as operações Query, Mutation do GraphQL.

Query

A query é usada para buscar informações. Com GraphQL, você solicita apenas os dados que precisa, evitando carregar informações desnecessárias. A seguir temos um código que mostra como buscar nomes e e-mails de pessoas cadastradas:

class MyHomePage extends StatelessWidget {
  final String query = (
    """
    query { 
      users { 
        nome email 
      } 
    }
    """
);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GraphQL Example')),
      body: Query(
        options: QueryOptions(
          document: gql(query), //aqui você irá fazer o controle da query
        ),
        builder: (result, {fetchMore, refetch}) {
          if (result.hasException) {
            return Text('Erro: ${result.exception}');
          }

          if (result.isLoading) {
            return CircularProgressIndicator();
          }

          final users = result.data!['users'];
          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(users[index]['nome']),
                subtitle: Text(users[index]['email']),
              );
            },
          );
        },
      ),
    );
  }
}

Nesse exemplo, a Query solicita apenas os campos nome e email dos pessoas cadastradas, e com apenas uma requisição, conseguimos o que queríamos.

Isso ajuda a evitar o overfetching, que ocorre quando recebemos mais dados do que precisamos, além disso evita o underfetching porque não teremos o risco de receber menos informações do que precisamos.

Se tivéssemos que buscar outros dados, como por exemplo os posts, bastaria colocar o que precisamos dentro da query, teríamos algo assim:

final String query = (
  """
  query ($userId: Int) {
    users {
      email
    }
    posts(userID: $userId){
      descricao

    }
  }
  """
);

Com apenas uma requisição, conseguimos consumir dados de fontes diferentes, incrível né?

Já aprendemos a buscar os dados da nossa API GraphQL, agora, chegou o momento de realizarmos outra operação.

Mutation

A mutation é usada para criar, atualizar ou excluir dados, funcionando como os métodos POST, PUT e DELETE no REST. Veja como utilizá-la:

class MyHomePage extends StatelessWidget {
  final String createUserMutation = '''
  mutation CreateUser(\$nome: String!, \$email: String!) {
    createUser(nome: \$nome, email: \$email) {
      id
      nome
      email
    }
  }
''';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('GraphQL Mutation Example')),
      body: Mutation(
        options: MutationOptions(
          document: gql(createUserMutation),
          variables: {'nome': 'Nova pessoa', 'email': '[email protected]'},
        ),
        builder: (runMutation, result) {
          return ElevatedButton(
            onPressed: () {
              runMutation({'nome': 'Nova pessoa', 'email': '[email protected]'});
            },
            child: Text('Adicionar pessoa'),
          );
        },
      ),
    );
  }
}

O widget Mutation do graphql_flutter executa a operação de cadastrar uma nova pessoa usuária ao pressionarmos o botão.

Subscription

É uma operação presente no GraphQL que nos permite ouvir alterações em tempo real. Ou seja, sempre que algo mudar no servidor, a aplicação recebe uma atualização automaticamente.

Diferente das outras operações, a subscription possui alguns detalhes mais complexos de implementação.

Por isso, recomendamos que você consulte a documentação oficial para entender esses detalhes: graphql_flutter.

E pronto! Finalizamos as operações básicas da nossa API criada com GraphQL.

Com o que vimos até agora, podemos concluir que o GraphQL resolve algumas das limitações comuns encontradas em APIs REST. No entanto, isso não significa que o GraphQL seja a solução ideal para todos os casos.

GraphQL ou REST?

O REST é uma opção simples, pois usa métodos HTTP como GET, POST, PUT e DELETE, facilitando a implementação, especialmente em projetos menores ou com requisitos claros. Sua ampla disponibilidade de ferramentas e documentação também favorece equipes que buscam agilidade.

O REST é menos flexível em sistemas complexos, pois exige múltiplas requisições, o que pode aumentar a latência e dificultar a manutenção em aplicações escaláveis ou com dados de várias fontes.

O GraphQL é flexível, permitindo ao cliente buscar exatamente o necessário em uma única requisição, ideal para sistemas complexos e aplicativos móveis que exigem economia de dados e melhor performance.

O GraphQL exige mais cuidado na configuração e tem menos profissionais experientes, o que pode dificultar sua adoção, especialmente para equipes sem familiaridade ou quando os benefícios não são claros.

No final, não existe uma escolha unânime. A decisão deve ser tomada com base nas necessidades específicas do projeto e nas capacidades da equipe de desenvolvimento.

Estamos aqui para te ajudar!

Escolher entre usar uma API REST ou GraphQL pode ser um desafio, e a melhor escolha vai depender das necessidades específicas da sua aplicação e do seu time.

Mas, independentemente de qual caminho você escolher, estamos aqui para te apoiar, a seguir temos alguns materiais que separamos para você:

Boa sorte na sua jornada!

Mikael Diniz
Mikael Diniz

Atualmente estou cursando Ciência da Computação na UFMA e, sou apaixonado por programação, games, matemática e basquete.

Veja outros artigos sobre Mobile