Async/Await: escrevendo código assíncrono de forma limpa

Async/Await: escrevendo código assíncrono de forma limpa

A cadeia de .then resolveu a pirâmide mas espalhou os valores por escopos separados. O async/await desfaz essa costura: o código assíncrono volta a ser lido de cima para baixo, com try/catch comum e for...of que espera de verdade — inclusive o clássico await dentro de forEach, que não espera nada.
Javascript

• • 18 min de leitura

As Promises resolveram o Callback Hell. Mas encadear muitos .then() ainda pode ficar confuso quando há lógica condicional, loops ou múltiplas variáveis que precisam ser compartilhadas entre etapas.

O async/await, introduzido no ES2017, é açúcar sintático em cima das Promises — ele não substitui as Promises, apenas fornece uma sintaxe mais limpa para trabalhar com elas. O resultado é código assíncrono que se lê como código síncrono, sem perder nenhum dos benefícios das Promises.

A palavra-chave async

async transforma uma função em uma função assíncrona. Uma função async sempre retorna uma Promise, mesmo que você não use return explicitamente ou retorne um valor simples:

// Função síncrona comum
function somar(a, b) {
  return a + b;
}
console.log(somar(2, 3)); // 5

// Função assíncrona — retorna uma Promise
async function somarAsync(a, b) {
  return a + b;
}

somarAsync(2, 3).then(resultado => console.log(resultado)); // 5

// Também funciona com arrow functions
const multiplicar = async (a, b) => a * b;

A palavra-chave await

await só pode ser usado dentro de funções async. Ele pausa a execução da função até que a Promise seja resolvida — e retorna o valor resolvido:

function esperar(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function exemplo() {
  console.log("Início");
  await esperar(2000); // pausa aqui por 2 segundos
  console.log("2 segundos depois");
  await esperar(1000); // pausa mais 1 segundo
  console.log("Mais 1 segundo depois");
}

exemplo();
console.log("Este executa imediatamente — não espera a função async");

// Saída:
// Início
// Este executa imediatamente — não espera a função async
// (2 segundos)
// 2 segundos depois
// (1 segundo)
// Mais 1 segundo depois

O await pausa somente a função async atual — o restante do programa continua normalmente.

Comparando Promises com async/await

O mesmo código escrito das duas formas:

function buscarUsuario(id) {
  return new Promise(resolve =>
    setTimeout(() => resolve({ id, nome: "Ana", plano: "premium" }), 800)
  );
}

function buscarPedidos(usuarioId) {
  return new Promise(resolve =>
    setTimeout(() => resolve([{ id: 101 }, { id: 102 }]), 600)
  );
}

// ── Com Promises e .then() ──────────────────
function carregarDadosPromise() {
  return buscarUsuario(1)
    .then(usuario => {
      console.log(`Usuário: ${usuario.nome}`);
      return buscarPedidos(usuario.id);
    })
    .then(pedidos => {
      console.log(`Pedidos: ${pedidos.length}`);
    })
    .catch(erro => console.error(erro.message));
}

// ── Com async/await ─────────────────────────
async function carregarDadosAsync() {
  try {
    const usuario = await buscarUsuario(1);
    console.log(`Usuário: ${usuario.nome}`);

    const pedidos = await buscarPedidos(usuario.id);
    console.log(`Pedidos: ${pedidos.length}`);
  } catch (erro) {
    console.error(erro.message);
  }
}

A versão com async/await é mais legível — especialmente quando há lógica condicional entre as etapas.

Tratamento de erros com try/catch

Com async/await, o tratamento de erros volta a usar o familiar try/catch:

async function processarPedido(usuarioId, produtoId) {
  try {
    // Cada await pode lançar um erro se a Promise rejeitar
    const usuario = await buscarUsuario(usuarioId);
    const produto = await buscarProduto(produtoId);

    if (produto.estoque === 0) {
      throw new Error(`Produto "${produto.nome}" sem estoque.`);
    }

    const pedido = await criarPedido(usuario.id, produto.id);
    console.log(`✅ Pedido #${pedido.id} criado com sucesso!`);
    return pedido;

  } catch (erro) {
    console.error(`❌ Erro ao processar pedido: ${erro.message}`);
    throw erro; // repropaga se necessário
  } finally {
    console.log("Processo de pedido finalizado.");
  }
}

Lógica condicional — onde async/await brilha

O ponto onde async/await supera claramente o encadeamento de .then():

async function checkout(carrinho, cupom) {
  const usuario = await buscarUsuarioLogado();

  // Lógica condicional — muito mais clara que em .then()
  if (!usuario) {
    throw new Error("Usuário não autenticado.");
  }

  const itensVerificados = await verificarEstoque(carrinho);

  let total = itensVerificados.reduce((acc, item) => acc + item.preco * item.quantidade, 0);

  if (cupom) {
    try {
      const desconto = await validarCupom(cupom, total);
      total = total - desconto;
      console.log(`Cupom aplicado! Desconto: R$ ${desconto}`);
    } catch {
      console.warn("Cupom inválido ou expirado. Continuando sem desconto.");
    }
  }

  const frete = await calcularFrete(usuario.cep);
  total += frete.valor;

  console.log(`Total com frete: R$ ${total.toFixed(2)}`);
  console.log(`Previsão de entrega: ${frete.prazo}`);

  const pedido = await confirmarPedido({ itens: itensVerificados, total, usuario });
  return pedido;
}

Tente reescrever isso com .then() encadeados e você vai ver o quanto async/await ajuda.

Loops assíncronos

Async/await permite usar estruturas de controle normais com código assíncrono:

const ids = [1, 2, 3, 4, 5];

// ── Sequencial — aguarda cada um antes do próximo
async function carregarSequencial() {
  console.log("Carregando em sequência...");
  const resultados = [];

  for (const id of ids) {
    const usuario = await buscarUsuario(id); // aguarda antes de continuar
    resultados.push(usuario);
    console.log(`Carregado: ${usuario.nome}`);
  }

  return resultados;
}
// Tempo total: soma dos tempos individuais (~5s se cada um levar 1s)

// ── Paralelo — inicia todos ao mesmo tempo
async function carregarParalelo() {
  console.log("Carregando em paralelo...");
  const promises = ids.map(id => buscarUsuario(id));
  const resultados = await Promise.all(promises);
  resultados.forEach(u => console.log(`Carregado: ${u.nome}`));
  return resultados;
}
// Tempo total: tempo do mais lento (~1s)

Use sequencial quando cada operação depende da anterior. Use paralelo (Promise.all) quando as operações são independentes.

async/await com Promise.all

Você pode — e deve — combinar async/await com os métodos de Promise:

async function carregarDashboard(usuarioId) {
  // Inicia tudo em paralelo — eficiente
  const [usuario, pedidos, notificacoes, relatorio] = await Promise.all([
    buscarUsuario(usuarioId),
    buscarPedidos(usuarioId),
    buscarNotificacoes(usuarioId),
    gerarRelatorio(usuarioId),
  ]);

  return { usuario, pedidos, notificacoes, relatorio };
}

// Usando
async function main() {
  try {
    const dados = await carregarDashboard(42);
    console.log(`Bem-vindo, ${dados.usuario.nome}!`);
    console.log(`${dados.pedidos.length} pedidos encontrados.`);
    console.log(`${dados.notificacoes.length} notificações.`);
  } catch (erro) {
    console.error("Erro ao carregar dashboard:", erro.message);
  }
}

main();

Top-level await

No passado, await só podia ser usado dentro de funções async. Em módulos JavaScript modernos (.mjs ou com "type": "module" no package.json), você pode usar await diretamente no nível do módulo:

// Em um módulo ES moderno — sem precisar de função async
const dados = await fetch("https://api.exemplo.com/dados");
const json = await dados.json();
console.log(json);

Isso é muito útil em scripts de configuração e no Node.js moderno.

Erro comum — await em forEach

Um dos erros mais frequentes com async/await: usar await dentro de forEach não funciona como esperado:

const ids = [1, 2, 3];

// ❌ ERRADO — forEach não aguarda os awaits
async function errado() {
  ids.forEach(async (id) => {
    const usuario = await buscarUsuario(id);
    console.log(usuario.nome); // a ordem é imprevisível
  });
  console.log("Terminou?"); // executa ANTES dos awaits!
}

// ✅ CORRETO — use for...of para código sequencial
async function correto() {
  for (const id of ids) {
    const usuario = await buscarUsuario(id);
    console.log(usuario.nome); // ordem garantida
  }
  console.log("Terminou!"); // executa DEPOIS de todos
}

// ✅ CORRETO — use Promise.all para paralelo
async function paraleloCorreto() {
  const usuarios = await Promise.all(ids.map(id => buscarUsuario(id)));
  usuarios.forEach(u => console.log(u.nome));
  console.log("Terminou!");
}

Padrões avançados

Retry automático — tentar novamente em caso de falha:

async function comRetry(funcao, tentativas = 3, delay = 1000) {
  for (let i = 1; i <= tentativas; i++) {
    try {
      return await funcao();
    } catch (erro) {
      console.warn(`Tentativa ${i}/${tentativas} falhou: ${erro.message}`);

      if (i === tentativas) throw erro; // última tentativa — propaga o erro

      await new Promise(resolve => setTimeout(resolve, delay * i)); // espera antes de tentar de novo
    }
  }
}

// Uso
const dados = await comRetry(
  () => buscarDadosDaAPI(),
  3,    // 3 tentativas
  500,  // 500ms, 1000ms, 1500ms entre tentativas
);

Timeout em requisições:

async function comTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout após ${ms}ms`)), ms)
  );

  return Promise.race([promise, timeout]);
}

// Uso
try {
  const dados = await comTimeout(buscarDadosDaAPI(), 5000);
  console.log(dados);
} catch (erro) {
  console.error(erro.message); // "Timeout após 5000ms"
}

Exemplo completo — sistema de autenticação

// Simulando serviços
const authService = {
  async validarCredenciais(email, senha) {
    await new Promise(r => setTimeout(r, 800));
    if (email === "admin@email.com" && senha === "senha123") {
      return { token: "jwt_token_abc123", expiraEm: 3600 };
    }
    throw new Error("Credenciais inválidas.");
  },

  async obterPerfil(token) {
    await new Promise(r => setTimeout(r, 500));
    if (!token) throw new Error("Token ausente.");
    return {
      id: 1,
      nome: "Administrador",
      email: "admin@email.com",
      permissoes: ["ler", "escrever", "deletar"],
    };
  },

  async registrarAcesso(userId) {
    await new Promise(r => setTimeout(r, 200));
    const agora = new Date().toLocaleString("pt-BR");
    return { userId, acessoEm: agora, ip: "192.168.1.1" };
  },
};

// Função de login completa
async function login(email, senha) {
  console.log("🔐 Iniciando autenticação...");

  try {
    // Passo 1 — valida credenciais
    const auth = await authService.validarCredenciais(email, senha);
    console.log(`✅ Credenciais válidas. Token expira em ${auth.expiraEm}s`);

    // Passo 2 e 3 — busca perfil e registra acesso em paralelo
    const [perfil, registro] = await Promise.all([
      authService.obterPerfil(auth.token),
      authService.registrarAcesso(1),
    ]);

    console.log(`👤 Bem-vindo, ${perfil.nome}!`);
    console.log(`🔑 Permissões: ${perfil.permissoes.join(", ")}`);
    console.log(`📋 Acesso registrado em: ${registro.acessoEm}`);

    return { auth, perfil, registro };

  } catch (erro) {
    console.error(`❌ Falha no login: ${erro.message}`);
    throw erro;
  }
}

async function main() {
  // Login bem-sucedido
  await login("admin@email.com", "senha123");

  console.log("\n--- Tentativa com credenciais erradas ---\n");

  // Login com falha
  await login("hacker@email.com", "errada").catch(() => {
    console.log("Redirecionando para tela de erro...");
  });
}

main();

Resumo — quando usar cada abordagem

Situação Use
Código simples e linear async/await
Operações em paralelo async/await + Promise.all
Encadeamento simples .then() ou async/await
Eventos que ocorrem várias vezes Callbacks
Precisa de .race(), .any(), .allSettled() Combine com await
Loop que precisa ser sequencial for...of com await
Loop paralelo Promise.all + map com await

Tarefa para você

Construa um sistema de importação de dados em lotes com async/await:

// Contexto: você tem 50 usuários para importar para um banco de dados.
// Importar todos de uma vez pode sobrecarregar o servidor.
// Importe em lotes de 10, com 500ms de pausa entre os lotes.

async function importarEmLotes(usuarios, tamanhoDeLote = 10, pausaMs = 500) {
  // 1. Divida o array em lotes de 'tamanhoDeLote'
  // 2. Para cada lote, importe todos em paralelo (Promise.all)
  // 3. Aguarde 'pausaMs' antes do próximo lote
  // 4. Exiba o progresso: "Lote 1/5 concluído (10/50 usuários)"
  // 5. Retorne um relatório final: { sucesso, falha, total }
}

// Simule a função de importar um usuário:
async function importarUsuario(usuario) {
  await new Promise(r => setTimeout(r, Math.random() * 300 + 100));
  if (Math.random() < 0.1) throw new Error(`Falha ao importar ${usuario.nome}`);
  return { id: Math.random(), ...usuario };
}
Ver solução — importarEmLotes com relatório, sem perder o lote por uma falha
// A função simulada do enunciado, com semente fixa para o resultado
// ser reproduzível ao rodar o exemplo.
async function importarUsuario(usuario) {
  await new Promise((r) => setTimeout(r, Math.random() * 300 + 100));

  if (Math.random() < 0.1) {
    throw new Error(`Falha ao importar ${usuario.nome}`);
  }

  return { id: Math.random(), ...usuario };
}

const esperar = (ms) => new Promise((resolver) => setTimeout(resolver, ms));

// ---------------------------------------------------------------
// 1 — dividir em lotes
// ---------------------------------------------------------------
function emLotes(itens, tamanho) {
  const lotes = [];

  for (let i = 0; i < itens.length; i += tamanho) {
    lotes.push(itens.slice(i, i + tamanho));
  }

  return lotes;
}

// ---------------------------------------------------------------
// 2 a 5 — a importação
// ---------------------------------------------------------------
async function importarEmLotes(usuarios, tamanhoDeLote = 10, pausaMs = 500) {
  const lotes = emLotes(usuarios, tamanhoDeLote);

  const sucesso = [];
  const falha = [];

  for (const [indice, lote] of lotes.entries()) {
    // allSettled, não all: `Promise.all` rejeita no primeiro erro e
    // descarta o resultado dos outros nove, que JÁ FORAM importados.
    // O banco ficaria com registros que o relatório não conhece.
    const resultados = await Promise.allSettled(lote.map(importarUsuario));

    resultados.forEach((resultado, i) => {
      if (resultado.status === "fulfilled") {
        sucesso.push(resultado.value);
      } else {
        falha.push({
          usuario: lote[i],
          motivo: resultado.reason.message,
        });
      }
    });

    const processados = sucesso.length + falha.length;
    console.log(
      `Lote ${indice + 1}/${lotes.length} concluído ` +
      `(${processados}/${usuarios.length} usuários)`
    );

    // Sem pausa depois do último lote: seriam 500 ms de espera à toa
    // antes de devolver o relatório.
    if (indice < lotes.length - 1) await esperar(pausaMs);
  }

  return {
    sucesso: sucesso.length,
    falha: falha.length,
    total: usuarios.length,
    importados: sucesso,
    erros: falha,
  };
}

// ---------------------------------------------------------------
// Rodando
// ---------------------------------------------------------------
const usuarios = Array.from({ length: 50 }, (_, i) => ({
  nome: `Usuário ${String(i + 1).padStart(2, "0")}`,
  email: `usuario${i + 1}@exemplo.com`,
}));

const relatorio = await importarEmLotes(usuarios);

console.log(`\n${relatorio.sucesso} importados · ${relatorio.falha} falharam ` +
            `· ${relatorio.total} no total`);

relatorio.erros.forEach((e) => console.log(`  ❌ ${e.usuario.nome}: ${e.motivo}`));

// ---------------------------------------------------------------
// Por que o for...of e não um forEach
// ---------------------------------------------------------------
// `await` dentro de forEach não espera nada: o callback é async, o
// forEach ignora a promise devolvida e passa para o próximo na hora.
// Os 5 lotes disparariam juntos — exatamente o que o exercício quer
// evitar.
//
//   lotes.forEach(async (lote) => {
//     await Promise.allSettled(lote.map(importarUsuario)); // ❌ ninguém espera
//   });
//
// `for...of` com await pausa o laço de verdade. É o jeito de fazer
// trabalho assíncrono em SÉRIE.

// ---------------------------------------------------------------
// Uma alternativa melhor que lote fixo: pool com concorrência
// ---------------------------------------------------------------
// Lote fixo tem um problema: o lote inteiro espera pelo item mais
// lento antes de o próximo começar. Um pool mantém sempre N em voo.
async function comPool(itens, limite, tarefa) {
  const resultados = new Array(itens.length);
  let proximo = 0;

  async function trabalhador() {
    while (proximo < itens.length) {
      const indice = proximo++; // captura antes do await, senão dois pegam o mesmo
      try {
        resultados[indice] = { status: "ok", valor: await tarefa(itens[indice]) };
      } catch (erro) {
        resultados[indice] = { status: "erro", motivo: erro.message };
      }
    }
  }

  await Promise.all(Array.from({ length: limite }, trabalhador));
  return resultados;
}

const doPool = await comPool(usuarios.slice(0, 20), 5, importarUsuario);
console.log(`\nPool: ${doPool.filter((r) => r.status === "ok").length}/20 importados`);

Promise.all rejeita no primeiro erro — e as outras promessas continuam rodando, com o resultado descartado. Numa importação isso deixa registros gravados que o relatório não conhece. Quando você quer saber o que aconteceu com cada item, é allSettled.

async e await são açúcar sobre Promise, e lembrar disso evita os dois enganos mais comuns: uma função async devolve uma Promise mesmo quando o corpo é inteiramente síncrono, e o await só espera aquilo que ele próprio alcança — dentro de um forEach, não espera nada. Para operações independentes, a forma continua sendo Promise.all; esperar uma de cada vez só se justifica quando a chamada seguinte precisa do resultado da anterior.

Fontes e leituras recomendadas

Exercícios

Exercício 1

O map tem callback async e não há Promise.all. O que sai em A e em B — e alguma das duas lança erro?

const ids = [1, 2, 3];

async function carregar() {
  const usuarios = ids.map(async (id) => await buscarUsuario(id));

  console.log(usuarios.length);   // A
  console.log(usuarios[0].nome);  // B
}
Ver resposta

✓ Resposta: A imprime 3 e B imprime undefined — e nenhuma das duas lança erro, que é justamente o que torna o defeito difícil de achar. O map não sabe nada de assincronia: ele chama o callback e guarda o que ele devolver, e um callback async devolve sempre uma Promise. Então usuarios é um array de três promises, com length igual a 3, e usuarios[0] é um objeto Promise que simplesmente não tem a propriedade nome — acessar propriedade inexistente devolve undefined em silêncio, sem exceção nenhuma. A correção é envolver o array num await Promise.all(...), que espera as três e devolve os valores. Repare ainda numa redundância no próprio callback: async (id) => await buscarUsuario(id) tem um await que não serve para nada, já que a função async devolveria a promise de qualquer forma — ids.map(id => buscarUsuario(id)), ou simplesmente ids.map(buscarUsuario), produz exatamente o mesmo array.

Exercício 2

Cada busca leva 1 segundo. Quanto tempo leva cada um dos três blocos?

// A
const a = await buscarUsuario(1);
const b = await buscarUsuario(2);

// B
const [c, d] = await Promise.all([buscarUsuario(1), buscarUsuario(2)]);

// C
const p1 = buscarUsuario(1);
const p2 = buscarUsuario(2);
const e = await p1;
const f = await p2;
Ver resposta

✓ Resposta: A leva 2 segundos; B e C levam 1 segundo cada. O C é o que costuma surpreender, e ele revela o ponto central: await não inicia coisa nenhuma. A operação começa no instante em que buscarUsuario(1) é chamada — a promise já está correndo antes de qualquer await. Em C as duas partem juntas, e os dois await apenas colhem resultados que chegam praticamente ao mesmo tempo; quando o segundo é alcançado, a promise dele já está resolvida ou quase. Em A, ao contrário, a segunda chamada só acontece depois de o primeiro await devolver, e por isso os tempos se somam. Ou seja, a diferença entre paralelo e sequencial não está no await, está em onde a chamada é feita. Vale uma ressalva prática sobre C: se p2 rejeitar antes de o código chegar ao await p2, há um intervalo em que essa rejeição não tem observador, e alguns ambientes já disparam o aviso de rejeição não tratada — motivo pelo qual Promise.all é a forma preferida.

Exercício 3

buscarUsuario(-1) sempre rejeita. Qual das duas funções captura o erro?

async function A() {
  try {
    buscarUsuario(-1);
  } catch (erro) {
    console.log("A capturou");
  }
}

async function B() {
  try {
    return buscarUsuario(-1);
  } catch (erro) {
    console.log("B capturou");
  }
}
Ver resposta

✓ Resposta: Nenhuma das duas. Em A falta o await: a chamada apenas cria a promise e devolve na hora, o bloco try termina com sucesso, e a rejeição chega depois, quando o catch já não está mais no caminho — o resultado é um UnhandledPromiseRejection. Em B a armadilha é mais fina: o return devolve a promise para quem chamou antes de ela se assentar, então a função sai do try imediatamente e a rejeição vira problema do chamador, não deste catch. As duas se consertam com a mesma palavra: await buscarUsuario(-1) e return await buscarUsuario(-1). Esse é exatamente o caso em que return await não é redundante, apesar de os linters costumarem sinalizá-lo como tal — dentro de um try, é ele que mantém a função presente para capturar a falha. A regra que resume tudo: try/catch só enxerga aquilo que este mesmo bloco esperou.

Exercício 4

A operação falha nas três tentativas. Quanto tempo o comRetry gasta esperando entre elas, e por que a última tentativa não espera?

async function comRetry(funcao, tentativas = 3, delay = 1000) {
  for (let i = 1; i <= tentativas; i++) {
    try {
      return await funcao();
    } catch (erro) {
      if (i === tentativas) throw erro;
      await new Promise(resolve => setTimeout(resolve, delay * i));
    }
  }
}

await comRetry(() => buscarDadosDaAPI(), 3, 500);
Ver resposta

✓ Resposta: Gasta 1500 ms em esperas: 500 ms depois da primeira falha e 1000 ms depois da segunda. A terceira não espera porque a linha if (i === tentativas) throw erro sai antes — e é assim que deve ser: esperar depois da última tentativa seria adiar a notificação do erro sem tentar nada. O tempo total, claro, ainda soma as três execuções da própria operação. Repare que delay * i produz um recuo linear (500, 1000, 1500…), não exponencial; a versão usada em produção costuma ser delay * 2 ** (i - 1), que cresce bem mais rápido e é mais gentil com um servidor em dificuldade. Falta ainda um detalhe importante nessa implementação: quando muitos clientes falham ao mesmo tempo — porque o servidor caiu —, todos recuam pelo mesmo intervalo e voltam juntos, dando uma nova pancada exatamente no momento em que ele tenta se recuperar. A correção é somar um valor aleatório ao intervalo, o chamado jitter. E vale a regra que antecede tudo isso: só faz sentido repetir operações idempotentes — repetir um pagamento pode cobrar duas vezes.

Exercício 5

Não há promise nenhuma aqui — o await recebe o número 42. Em que ordem as três linhas aparecem?

async function f() {
  console.log("1");
  const x = await 42;
  console.log("2", x);
}

f();
console.log("3");
Ver resposta

✓ Resposta: Sai 1, 3, e só então 2 42. O await não verifica se o valor é uma promise para decidir se pausa: ele sempre suspende a função e agenda a continuação como microtask, envolvendo o valor comum numa promise já resolvida. Então await 42 devolve 42, sim, mas só na próxima volta do event loop — e tudo o que estiver depois da chamada de f(), no código síncrono, roda antes. É o mesmo mecanismo do artigo sobre o Event Loop, visto por dentro de uma função async: a parte antes do primeiro await é síncrona, e daí em diante são microtasks. A consequência prática aparece quando se acrescenta um await a uma função que não tinha nenhum: tudo o que vem depois dele passa a executar mais tarde, e código de fora que dependia da ordem antiga muda de comportamento sem que uma linha dele tenha sido tocada.

Comentários

Mais em Javascript

Segurança em aplicações web
Segurança em aplicações web

Segurança não é etapa final, é decisão em cada linha. O artigo percorre as…

Callbacks: o começo de tudo
Callbacks: o começo de tudo

Antes das Promises, esperar era passar uma função para ser chamada depois. O…

Revisão + Projeto Final: Produção Real
Revisão + Projeto Final: Produção Real

Deploy, segurança e performance só existem depois que a aplicação existe, e…