DEV Community

Cover image for Azure Functions com TypeScript: da criação ao deploy
Bruno Mestres
Bruno Mestres

Posted on

Azure Functions com TypeScript: da criação ao deploy

Quando pensamos em desenvolver uma funcionalidade para uma aplicação, normalmente imaginamos uma API, um servidor e toda a infraestrutura necessária para manter esse serviço funcionando.

Mas será que precisamos de uma API tradicional para executar qualquer tipo de tarefa?

Imagine que precisamos receber um webhook, executar uma rotina agendada ou processar um arquivo enviado para um serviço de armazenamento.

É nesse tipo de cenário que o Azure Functions pode ser uma alternativa interessante.

Neste artigo, quero compartilhar um pouco da experiência utilizando Azure Functions com TypeScript, passando pela criação do projeto via CLI, estruturação, integração com Azure Blob Storage e publicação na nuvem.

O que é Azure Functions e quando utilizar?

Azure Functions é um serviço de computação serverless da Microsoft Azure, que permite executar código sem precisar gerenciar diretamente toda a infraestrutura de servidores.

Isso não significa que não existam servidores, mas que boa parte da responsabilidade de provisionamento e gerenciamento fica com a plataforma.

Uma das principais características do serviço é a execução orientada a eventos, utilizando mecanismos chamados triggers.

Alguns exemplos:

  • HTTP Trigger: executa uma função ao receber uma requisição HTTP, sendo interessante para endpoints e webhooks.
  • Timer Trigger: executa rotinas em horários ou intervalos definidos.
  • Blob Trigger: reage à criação ou atualização de arquivos em um Blob Storage.
  • Queue Trigger: processa mensagens recebidas de uma fila.

Isso permite desenvolver funcionalidades que são executadas quando determinadas condições acontecem.

Por exemplo, imagine um sistema que precisa gerar relatórios diariamente. Em vez de manter um processo próprio responsável pelo agendamento, podemos utilizar uma Function com Timer Trigger.

Outro cenário seria receber notificações de uma plataforma de pagamentos. Nesse caso, um HTTP Trigger poderia receber o webhook e iniciar o processamento da informação.

Mas isso significa que Azure Functions serve apenas para tarefas pequenas?

Não necessariamente.

Uma Function também pode executar regras de negócio mais complexas. O importante é avaliar o comportamento da carga de trabalho, o tempo de execução, os custos e as necessidades de escalabilidade.

Criando nosso projeto com Azure Functions Core Tools

Uma das coisas que achei bastante interessantes ao utilizar Azure Functions foi a facilidade de criar e gerenciar projetos por meio da CLI.

O Azure Functions Core Tools disponibiliza o comando func, que permite criar projetos, adicionar funções, executar o ambiente local e publicar aplicações no Azure.

Para começar, precisamos ter o Node.js e o Azure Functions Core Tools instalados.

Vamos criar um projeto utilizando TypeScript e o modelo de programação v4.

func init azure-functions-example \
  --worker-runtime node \
  --language typescript \
  --model V4
Enter fullscreen mode Exit fullscreen mode

Depois, acessamos a pasta:

cd azure-functions-example
Enter fullscreen mode Exit fullscreen mode

Agora podemos criar nossa primeira função utilizando um HTTP Trigger:

func new \
  --template "HTTP trigger" \
  --name UploadFile \
  --authlevel function
Enter fullscreen mode Exit fullscreen mode

Com poucos comandos, já temos a estrutura inicial do projeto preparada.

Outro detalhe interessante é que não precisamos criar manualmente toda a configuração de uma aplicação Node.js do zero.

O Core Tools gera os arquivos necessários para trabalhar com o runtime do Azure Functions.

Entendendo a estrutura do projeto

Utilizando TypeScript e o modelo de programação v4, teremos uma estrutura inicial semelhante a esta:

azure-functions-example/
├── src/
│   └── functions/
│       └── UploadFile.ts
├── host.json
├── local.settings.json
├── package.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Alguns desses arquivos merecem atenção.

host.json

Contém configurações globais do runtime, permitindo ajustar comportamentos que afetam a aplicação de funções.

local.settings.json

Armazena configurações utilizadas durante o desenvolvimento local, incluindo variáveis de ambiente e conexões com serviços.

Como esse arquivo pode conter credenciais, ele não deve ser enviado para o repositório.

src/functions

É onde podemos organizar as funções responsáveis por responder aos eventos da aplicação.

No modelo v4, essa estrutura é flexível. Isso significa que também podemos separar serviços, regras de negócio e integrações em outros diretórios, conforme o projeto cresce.

Um ponto importante é que várias funções podem fazer parte de uma mesma Function App, compartilhando configurações e sendo implantadas como uma unidade.

Exemplo prático: HTTP Trigger com Azure Blob Storage

Vamos imaginar um cenário simples: precisamos receber um conteúdo por HTTP e armazená-lo no Azure Blob Storage.

O Blob Storage é um serviço de armazenamento de objetos da Azure, bastante utilizado para arquivos, documentos, imagens e outros dados não estruturados.

Nosso fluxo será o seguinte:

  1. Um cliente envia uma requisição HTTP.
  2. Nossa Azure Function recebe os dados.
  3. O conteúdo é enviado ao Blob Storage.
  4. A função retorna uma resposta indicando o resultado da operação.

Para isso, vamos utilizar o SDK oficial da Azure:

npm install @azure/storage-blob
Enter fullscreen mode Exit fullscreen mode

Agora podemos implementar nossa Function.

import {
  app,
  HttpRequest,
  HttpResponseInit,
  InvocationContext,
} from "@azure/functions";

import { BlobServiceClient } from "@azure/storage-blob";
import { randomUUID } from "node:crypto";

export async function uploadFile(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  try {
    const body = (await request.json()) as {
      content?: string;
    };

    if (typeof body.content !== "string" || !body.content) {
      return {
        status: 400,
        jsonBody: {
          error: "O campo content é obrigatório.",
        },
      };
    }

    if (Buffer.byteLength(body.content, "utf8") > 1024 * 1024) {
      return {
        status: 413,
        jsonBody: {
          error: "Conteúdo acima de 1 MB.",
        },
      };
    }

    const connectionString =
      process.env.BLOB_STORAGE_CONNECTION_STRING;

    if (!connectionString) {
      throw new Error("Conexão com Blob Storage não configurada");
    }

    const blobServiceClient =
      BlobServiceClient.fromConnectionString(connectionString);

    const containerClient =
      blobServiceClient.getContainerClient("uploads");

    await containerClient.createIfNotExists();

    const fileName = `${randomUUID()}.txt`;

    const blockBlobClient =
      containerClient.getBlockBlobClient(fileName);

    await blockBlobClient.uploadData(
      Buffer.from(body.content, "utf8"),
      {
        blobHTTPHeaders: {
          blobContentType: "text/plain",
        },
      }
    );

    return {
      status: 201,
      jsonBody: {
        message: "Arquivo armazenado com sucesso",
        fileName,
      },
    };
  } catch (error) {
    context.error("Erro ao armazenar arquivo", error);

    return {
      status: 500,
      jsonBody: {
        error: "Erro interno ao armazenar arquivo",
      },
    };
  }
}

app.http("UploadFile", {
  methods: ["POST"],
  authLevel: "function",
  handler: uploadFile,
});
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, utilizamos app.http() para registrar nossa Function e configurar o HTTP Trigger.

Definimos que ela responderá a requisições POST e utilizamos o BlobServiceClient para realizar a comunicação com o serviço de armazenamento.

O nome do arquivo é gerado automaticamente e o conteúdo recebido é enviado para um container chamado uploads.

Para simplificar o exemplo, estamos armazenando texto. Em uma implementação real de upload de imagens ou documentos, poderíamos trabalhar com arquivos binários, validação de formato e outras medidas de segurança.

Configurando o armazenamento

Para realizar essa integração, precisamos configurar a conexão com o Blob Storage.

No arquivo local.settings.json, podemos adicionar a seguinte configuração:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "BLOB_STORAGE_CONNECTION_STRING": "UseDevelopmentStorage=true"
  }
}
Enter fullscreen mode Exit fullscreen mode

Nesse exemplo, estamos utilizando o Azurite, que permite simular o Azure Storage durante o desenvolvimento local.

Precisamos ter o Azurite instalado e em execução para que essas configurações funcionem.

Caso utilizemos uma conta real do Azure Storage, devemos configurar a conexão correspondente.

Em produção, uma alternativa mais segura é utilizar identidades gerenciadas, evitando depender de chaves de acesso armazenadas na aplicação.

Executando localmente

Com o ambiente configurado e as dependências instaladas, podemos iniciar o projeto:

npm start
Enter fullscreen mode Exit fullscreen mode

O comando utiliza os scripts definidos no projeto TypeScript para compilar e iniciar o runtime local.

Nossa Function estará disponível em um endereço semelhante a:

http://localhost:7071/api/UploadFile
Enter fullscreen mode Exit fullscreen mode

Podemos enviar uma requisição de teste com o seguinte JSON:

{
  "content": "Meu primeiro arquivo com Azure Functions!"
}
Enter fullscreen mode Exit fullscreen mode

Se tudo estiver configurado corretamente, a Function armazenará o conteúdo no Blob Storage e retornará o nome do arquivo criado.

O nível authLevel: "function" exige uma chave ao chamar o endpoint publicado no Azure. Durante o desenvolvimento local pelo Core Tools, essa exigência normalmente não é aplicada.

Vale lembrar que, em produção, uma chave de Function não substitui uma estratégia completa de autenticação e autorização.

Publicando nossa Function no Azure

Outro ponto que achei bastante prático foi o processo de deploy por meio da própria CLI.

Depois de desenvolver e testar a função, podemos utilizar o comando:

func azure functionapp publish <NOME_DA_FUNCTION_APP>
Enter fullscreen mode Exit fullscreen mode

Esse comando publica o projeto em uma Function App previamente criada no Azure.

Para realizar a publicação, precisamos estar autenticados em nossa conta e ter os recursos de nuvem devidamente provisionados.

Também precisamos configurar as variáveis de ambiente utilizadas pela aplicação, já que o arquivo local.settings.json não é publicado automaticamente como configuração de produção.

A vantagem é conseguir trabalhar com criação, execução e publicação do projeto utilizando ferramentas de linha de comando, sem depender exclusivamente do portal da Azure.

Para projetos maiores, o ideal é complementar esse processo com uma pipeline de CI/CD, automatizando testes e implantações.

Azure Functions ou uma API tradicional?

Depois desse exemplo, podemos chegar a uma pergunta importante: por que utilizar uma Azure Function se poderíamos implementar a mesma funcionalidade em uma API com Express ou NestJS?

Na prática, ambas conseguem receber uma requisição HTTP e armazenar um arquivo no Blob Storage.

A diferença não está exatamente no que conseguimos implementar, mas em como queremos executar, gerenciar e escalar essa funcionalidade.

Azure Functions API tradicional
Execução orientada a triggers Normalmente executada como serviço HTTP
Escalabilidade gerenciada conforme o plano Escalabilidade configurada conforme a infraestrutura
Interessante para eventos e rotinas Interessante para APIs com muitos recursos e fluxos
Pode ter cold start em determinados planos Pode manter instâncias sempre disponíveis
Cobrança dependente do plano e consumo Custos dependentes da hospedagem escolhida

Imagine que temos um sistema de e-commerce com autenticação, catálogo de produtos, carrinho e pedidos.

Uma API com NestJS pode ser uma escolha bastante interessante para organizar esses diferentes módulos e regras de negócio.

Agora imagine que, toda vez que um pedido é finalizado, precisamos gerar um documento e armazená-lo no Blob Storage.

Essa tarefa poderia ser executada por uma Azure Function, acionada por um evento ou mensagem de fila, sem precisar ficar dentro do fluxo principal da API.

E aqui aparece um detalhe importante: não precisamos necessariamente escolher uma abordagem e abandonar a outra.

Uma aplicação pode utilizar uma API tradicional para suas operações principais e Azure Functions para determinadas integrações, tarefas assíncronas e rotinas.

Quais são os trade-offs?

Apesar das facilidades, utilizar Azure Functions também exige algumas considerações.

Uma delas é o cold start, que pode ocorrer em determinados planos quando uma instância precisa ser inicializada para executar a função. Isso pode adicionar latência à primeira requisição.

Também precisamos considerar limites e configurações de tempo de execução, que variam conforme o plano de hospedagem.

Outro ponto é a observabilidade. Mesmo trabalhando com funções relativamente pequenas, precisamos acompanhar falhas, logs e métricas, principalmente quando temos várias funções integradas a diferentes serviços.

Além disso, o modelo serverless não significa automaticamente menor custo. Para cargas constantes ou muito intensas, outras opções de hospedagem podem ser mais vantajosas.

A escolha depende das necessidades da aplicação.

Conclusão

Explorar Azure Functions com TypeScript me mostrou como podemos simplificar a implementação de determinadas funcionalidades, principalmente quando trabalhamos com eventos, integrações e rotinas.

Um dos pontos que mais gostei foi a experiência com o Azure Functions Core Tools. Utilizar o comando func para criar projetos, adicionar novas funções, executar localmente e realizar o deploy torna o desenvolvimento bastante prático.

A integração com serviços como Azure Blob Storage também abre possibilidades para construir fluxos sem precisar manter uma aplicação inteira dedicada a cada responsabilidade.

Mas acredito que o principal aprendizado seja entender onde essa abordagem faz sentido.

Azure Functions não é necessariamente uma substituição para APIs tradicionais. É mais uma alternativa arquitetural que podemos utilizar conforme o problema que precisamos resolver.

E, como em outras decisões de arquitetura de software, o mais importante é entender os benefícios, os custos e os trade-offs antes de escolher a solução.


Referências

Top comments (0)