skip to content
Gustavo Bindandi

Encurtador de Link na AWS (API Gateway + Lambda)

Nesta leitura irei detalhar como eu criei um encurtador de link

Por que decidi criar o Encurtador?

Na Ares Mídia, desenvolvi um sistema interno para gerenciar a carteira de clientes e gerar cobranças mensais automaticamente. Além disso, criei soluções internas para que as empresas mensalistas possam utilizar essas ferramentas de forma pré-paga. À medida que o tempo passa e tenho novas ideias, incorporo-as ao sistema, desafiando-me constantemente e mantendo a esperança de que um dia um cliente usará essa solução e ela funcionará para ele.

Encurtadores de link são ferramentas para criar links mais curtos. São muito utilizados para criar links curtos (e muitas vezes personalizados) no YouTube, WhatsApp, Facebook, Linktree, entre outros. (fonte: Olhar Digital).


Desenvolvimento e identificação da Infraestrutura

A primeira coisa que fiz foi criar dois schemas no meu banco de dados. Estou usando o Prisma para gerenciar meu banco de dados MySQL. Em um desses schemas, criei um modelo para salvar os logs de acesso desse link encurtado e, no outro, um modelo para salvar o link de origem e o novo link encurtado. Fiz isso para poder gerar um relatório para o usuário sobre quantos cliques, a região, e outras informações.

Esse é o schema:


model GeradorLink {
  id           Int       @id @default(autoincrement())
  user_id      Int
  user         User      @relation(fields: [user_id], references: [id])
  link         String
  identifier   String
  link_encoded String?
  created      DateTime  @default(now())
  updated      DateTime  @updatedAt
  deleted      DateTime?

  LinkAnalysis LinkAnalysis[]

  @@map("gerador_link")
}

model LinkAnalysis {
  id           Int         @id @default(autoincrement())
  link_id      Int
  link         GeradorLink @relation(fields: [link_id], references: [id])
  ip           String?
  country      String?
  region       String?
  city         String?
  isp          String?
  organization String?
  latitude     String?
  longitude    String?
  origin       String?
  user_agent   String?
  created      DateTime    @default(now())
  updated      DateTime    @updatedAt
  deleted      DateTime?

  @@map("link_analysis")
}

Depois de criar essas duas tabelas no banco de dados, implementei uma rota e um controller no meu backend com NestJs para salvar esse log e redirecioná-lo para o site de origem. Funcionou perfeitamente, mas a URL encurtada acabou ficando assim: “api.meusite.com/encurt/lkp3129”, onde “lkp3129” é o código único de identificação e o restante é o endereço da minha API.

O problema é que, desse jeito, o link ficou enorme e ruim para o usuário. Não dá para chamar isso de encurtador haha.

Foi aí que parei para pensar em uma solução e me lembrei do API Gateway com trigger no Lambda 🤯.


Criando a arquitetura na AWS

Antes de mais nada, investiguei o custo de implementação dessa arquitetura e descobri que era bastante acessível, o que já foi um ótimo ponto de partida.

Inicialmente, procurei por referências e trabalhos similares para entender melhor como outros desenvolvedores haviam abordado problemas parecidos. Encontrei uma variedade de conteúdos que me ajudaram a formular um plano de ação.

O primeiro passo foi criar um recurso no API Gateway. alt text

O recurso que configurei continha um /{cod+}, o que permite a dinamicidade na rota, como em link.com/0000 ou link.com/1111. No API Gateway, essa é a maneira de configurar uma rota dinâmica. Apesar de precisar apenas do método GET para essa solução, optei por configurar o método ANY, para adaptar novas ideias futuras.

Depois de estabelecer o recurso no API Gateway, passei para a criação de uma function no AWS Lambda chamada “URLShortenerFunction”. Esta function é responsável por extrair o código do {cod}, consultar no banco de dados o URL de origem, registrar o acesso em um log, e finalmente, redirecionar o usuário para o URL de origem.

No schema de logs, você pode observar campos para registrar informações como IP e User Agent. Buscando por muitos minutos eu encontrei uma API grátis que me ajudou com essas informações.

A seguir, apresento o código implementado na function Lambda:

exports.handler = async (event) => {
  try {
    // Exemplo de consulta: selecione todos os registros de uma tabela
    const urlBase = 'https://site.com'
    const codigo = event.pathParameters.cod;
    const link = `${urlBase}/${codigo}`;

    const result = await knex('gerador_link').select('*').where('link_encoded', link).first();
    const linkOrigem = result.link
    const ipAccessed = event.requestContext.identity.sourceIp;
    const userAgent = event.requestContext.identity.userAgent;

    try {
      const dataIp = (await axios.get(`http://ip-api.com/json/${ipAccessed}`)).data;
      if (dataIp.status == 'fail') {
        await knex('link_analysis').insert({
          link_id: result.id,
          ip: ipAccessed,
          origin: result.link,
          user_agent: userAgent,
          created: new Date(),
          updated: new Date(),
        })
      } else {
        const paylog = {
          link_id: result.id,
          ip: ipAccessed,
          country: dataIp.country,
          region: dataIp.regionName,
          city: dataIp.city,
          isp: dataIp.isp,
          organization: dataIp.org,
          latitude: String(dataIp.lat),
          longitude: String(dataIp.lon),
          origin: result.link,
          user_agent: userAgent,
          created: new Date(),
          updated: new Date(),
        };

        await knex('link_analysis').insert(paylog);
      }
    } catch (e) {
      console.log(e)
      await knex('link_analysis').insert({
        link_id: result.id,
        ip: ipAccessed,
        origin: result.link,
        user_agent: userAgent,
        created: new Date(),
        updated: new Date(),
      })
    }

    return {
      statusCode: 301,
      headers: {
        Location: linkOrigem
      },
      body: ''
    };
  } catch (error) {
    console.error('Erro ao executar a consulta:', error);
    return {
      statusCode: 500,
      body: JSON.stringify(error)
    };
  }
};

No código que desenvolvi, há um tratamento especial para os casos em que a API externa não consegue encontrar o endereço IP do usuário. Entendo que é essencial não perder registros de acesso, então decidi salvar cada clique, mesmo sem a informação do IP.

Durante o desenvolvimento, refleti sobre a melhor forma de gerenciar a conexão com o banco de dados. Havia a opção de instanciar a conexão MYSQL diretamente dentro da function Lambda ou criar uma rota específica no backend para gerenciar os logs. As duas abordagens têm seus prós e contras, e qualquer escolha seria correta.

Optar por utilizar o backend poderia aumentar a segurança, considerando que o banco de dados contém informações sensíveis. No entanto, isso poderia também introduzir um atraso significativo no redirecionamento para o site de origem, possivelmente medido em milissegundos ou até segundos.

Decidi manter a conexão ao banco de dados diretamente no Lambda. Para aumentar a segurança, utilizei variáveis de ambiente para gerenciar as credenciais de acesso ao banco. Também fui cuidadoso com as querys SQL para mitigar o risco de exploração através de SQL Injection.

Após implementar e testar o código no Lambda, e configurar o API Gateway, prossegui para estabelecer uma trigger no API Gateway.

alt text

Após configurar e testar todos os componentes, eu implementei minha API no API Gateway. 🥳

Agora, sempre que alguém acessa a rota, como meusite.com/0000, o API Gateway processa esse código dinâmico (0000) e aciona minha function Lambda, que por sua vez, cuida de todo o processamento necessário e efetua o redirecionamento. E com isso, o encurtador de links funciona!

E pronto! Essa é minha arquitetura

Dominio curto

Com o API Gateway já configurado, definir um domínio curto foi um processo simples. Contratei um domínio com um nome atrativo para links curtos, algo no estilo de bit.ly, li.ly, ou hs.ly. Em seguida, configurei esse domínio no API Gateway. alt text

E assim, finalizei a configuração do meu domínio. Agora é só ser feliz.


Resultado já integrado no front-end

Assim ficou meu sistema interno com essa nova solução de encurtar link: alt text

alt text

Conclusão

😁 Fico feliz por ter feito essa solução, foi um desafio interessante. Me peguei quebrando a cabeça diversas vezes, principalmente em entender como eu iria criar essa solução sem ter um servidor rodando 24 horas e com baixo recurso.

Estou bem orgulhoso desse meu trabalho e eu espero que possa ser utilizado muitas vezes.

Agradeço por ler este artigo! Até breve.