Como construir uma API RESTful com Edge Functions e Edge SQL

Este guia demonstra como construir uma API RESTful completa para gerenciamento de tarefas usando Azion Edge Functions e Edge SQL. Você aprenderá a implementar operações CRUD (Create, Read, Update, Delete) completas no edge, fornecendo endpoints de API rápidos e escaláveis globalmente.

Requisitos

Antes de começar, certifique-se de ter:

Primeiros passos

Passo 1: Criar um novo banco de dados Edge SQL

  1. Crie um banco de dados Edge SQL usando a API da Azion
Terminal window
curl --location 'https://api.azion.com/v4/edge_sql/databases' \
--header 'Authorization: Token [TOKEN VALUE]' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-database"
}'

Você deve receber a seguinte resposta:

{
"state": "pending",
"data": {
"id": 118,
"name": "my-database",
"client_id": "6832h",
"status": "creating",
"created_at": "2024-04-18T11:22:59.468536Z",
"updated_at": "2024-04-18T11:22:59.468586Z",
"deleted_at": null
}
}

Para mais detalhes, veja a documentação do Edge SQL.

Passo 2: Configurar o schema do banco de dados

Você pode usar os seguintes comandos SQL para configurar o schema do banco de dados.

  1. Conecte-se à sua instância Edge SQL e crie a tabela de tarefas:
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
  1. Opcionalmente, insira alguns dados de exemplo para teste:
INSERT INTO tasks (title, completed) VALUES
('Complete API documentation', FALSE),
('Test edge function deployment', FALSE),
('Review security configurations', TRUE);

Código

Este é um exemplo de código de como criar uma API RESTful com Azion Edge Functions e Edge SQL. O exemplo completo do código você pode encontrar nesse repositório do GitHub.

Integração com banco de dados

import { useExecute, useQuery } from "azion/sql";
const DATABASE_NAME = process.env.DATABASE_NAME || "tasks";
export interface Task {
id: number;
title: string;
completed: boolean;
}
export const getTasks = async (): Promise<Task[]> => {
const { data, error } = await useQuery(DATABASE_NAME, [
"SELECT * FROM tasks",
]);
if (error) {
throw error;
}
return (
data?.results?.[0]?.rows?.map(
(row) =>
({
id: row[0],
title: row[1],
completed: row[2] === 1,
} as Task)
) || []
);
};
export const getTask = async (id: number): Promise<Task | null> => {
const { data, error } = await useQuery(DATABASE_NAME, [
`SELECT * FROM tasks WHERE id = ${id}`,
]);
if (error) {
throw error;
}
return (
(data?.results?.[0]?.rows?.[0] &&
({
id: data?.results?.[0]?.rows?.[0][0],
title: data?.results?.[0]?.rows?.[0][1],
completed: data?.results?.[0]?.rows?.[0][2] === 1,
} as Task)) ||
null
);
};
export const createTask = async (task: Omit<Task, "id">): Promise<Task> => {
const { data, error } = await useExecute(DATABASE_NAME, [
`INSERT INTO tasks (title, completed) VALUES ('${task.title}', ${task.completed}) RETURNING id`,
]);
if (error) {
throw error;
}
const newId = data?.results?.[0]?.rows?.[0]?.[0] || "";
return { ...task, id: Number(newId) };
};
export const updateTask = async (
id: number,
task: Partial<Omit<Task, "id">>
): Promise<Task | null> => {
const existingTask = await getTask(id);
if (!existingTask) {
return null;
}
const updatedTitle =
task.title !== undefined ? task.title : existingTask.title;
const updatedCompleted =
task.completed !== undefined ? task.completed : existingTask.completed;
const { data, error } = await useExecute(DATABASE_NAME, [
`UPDATE tasks SET title = '${updatedTitle}', completed = ${updatedCompleted} WHERE id = ${id}`,
]);
if (error) {
throw error;
}
return { ...existingTask, title: updatedTitle, completed: updatedCompleted };
};
export const deleteTask = async (id: number): Promise<boolean> => {
const { data, error } = await useExecute(DATABASE_NAME, [
`DELETE FROM tasks WHERE id = ${id}`,
]);
if (error) {
throw error;
}
return data?.state === "executed" || data?.state === "pending";
};

Aplicação

import { Hono } from 'hono';
import { fire } from 'hono/service-worker';
import {
createTask,
deleteTask,
getTask,
getTasks,
updateTask,
} from './db';
const app = new Hono();
app.get('/tasks', async (c) => {
try {
const tasks = await getTasks();
return c.json(tasks);
} catch (error) {
return c.json({ error: 'Failed to fetch tasks' }, 500);
}
});
app.get('/tasks/:id', async (c) => {
try {
const { id } = c.req.param();
const task = await getTask(Number(id));
if (!task) {
return c.json({ error: 'Task not found' }, 404);
}
return c.json(task);
} catch (error) {
return c.json({ error: 'Failed to fetch task' }, 500);
}
});
app.post('/tasks', async (c) => {
try {
const { title, completed } = await c.req.json();
const newTask = await createTask({ title, completed: completed || false });
return c.json(newTask, 201);
} catch (error) {
console.log(error);
return c.json({ error: 'Failed to create task' }, 500);
}
});
app.put('/tasks/:id', async (c) => {
try {
const { id } = c.req.param();
const { title, completed } = await c.req.json();
const updatedTask = await updateTask(Number(id), { title, completed });
if (!updatedTask) {
return c.json({ error: 'Task not found' }, 404);
}
return c.json(updatedTask);
} catch (error) {
return c.json({ error: 'Failed to update task' }, 500);
}
});
app.delete('/tasks/:id', async (c) => {
try {
const { id } = c.req.param();
const success = await deleteTask(Number(id));
if (!success) {
return c.json({ error: 'Task not found' }, 404);
}
return c.json({ message: 'Task deleted' });
} catch (error) {
return c.json({ error: 'Failed to delete task' }, 500);
}
});
fire(app);

Deploy na Azion

Passo 1: Autenticação na Azion

  1. Faça login na sua conta Azion via CLI:
Terminal window
azion login
  1. Siga os prompts de autenticação para conectar sua CLI com sua conta Azion.

Passo 2: Criar uma nova Edge Application

  1. Inicialize uma nova Edge Application:
Terminal window
azion init
  1. Selecione o template Hono Boilerplate para usar para sua aplicação browserless.

  2. Siga os prompts para configurar sua nova Edge Application.

Passo 3: Deploy da Edge Function

Deploy da sua RESTful API para a rede da Azion:

Terminal window
azion deploy

O processo de deploy irá:

  • Fazer upload do código da sua Edge Function
  • Configurar a edge application
  • Configurar regras de roteamento para todos os endpoints da API
  • Configurar conexões com o banco de dados
  • Fornecer um domínio único

Passo 4: Acesse sua API

Após o deploy, você receberá um domínio como https://xxxxxxx.map.azionedge.net. Sua API RESTful estará disponível nesta URL em alguns minutos após a propagação do DNS.

Entendendo a implementação

Estrutura da Edge Function

A Edge Function da API RESTful inclui:

  1. Configuração do router: Manipulação de diferentes métodos HTTP e rotas
  2. Conexão com banco de dados: Conectando ao Edge SQL
  3. Operações CRUD: Implementação de operações create, read, update, delete
  4. Tratamento de erros: Gerenciamento de erros de banco de dados e requisição
  5. Formatação de resposta: Retorno de respostas JSON consistentes

Exemplo de operações de banco de dados

Veja como as operações de banco de dados são implementadas:

// Obter todas as tarefas
async function getAllTasks() {
const query = 'SELECT * FROM tasks ORDER BY created_at DESC';
const result = await database.query(query);
return result.rows;
}
// Criar uma nova tarefa
async function createTask(title, completed = false) {
const query = `
INSERT INTO tasks (title, completed)
VALUES (?, ?)
RETURNING *
`;
const result = await database.query(query, [title, completed]);
return result.rows[0];
}
// Atualizar uma tarefa
async function updateTask(id, updates) {
const { title, completed } = updates;
const query = `
UPDATE tasks
SET title = ?, completed = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
RETURNING *
`;
const result = await database.query(query, [title, completed, id]);
return result.rows[0];
}

Principais benefícios

  • Performance: Processamento de API no edge reduz latência
  • Escalabilidade: Escala automaticamente com a demanda
  • Distribuição global: Edge locations oferecem cobertura mundial
  • Consistência de dados: Edge SQL garante conformidade ACID
  • Eficiência de custo: Acesso e transferência de dados otimizados

Testando sua API RESTful

Passo 1: Teste operações CRUD

  1. Criar tarefas: Teste endpoint POST/tasks
  2. Ler tarefas: Teste endpoints GET/tasks e GET/tasks/
  3. Atualizar tarefas: Teste endpoint PUT/tasks/
  4. Excluir tarefas: Teste endpoint DELETE/tasks/

Passo 2: Teste tratamento de erros

  1. Dados inválidos: Teste com corpos de requisição malformados
  2. Recursos inexistentes: Teste com IDs de tarefa inválidos
  3. Erros de banco de dados: Teste tratamento de erros para problemas de conexão
  4. Erros de validação: Teste lógica de validação de entrada

Passo 3: Testes de performance

  1. Tempos de resposta: Meça tempos de resposta da API
  2. Requisições simultâneas: Teste múltiplas requisições simultâneas
  3. Performance do banco de dados: Monitore performance de consultas SQL
  4. Teste de carga: Teste com altos volumes de requisições

Exemplo de uso da API

Usando cURL

Terminal window
# Obter todas as tarefas
curl -X GET https://seu-dominio.map.azionedge.net/tasks
# Criar uma nova tarefa
curl -X POST https://seu-dominio.map.azionedge.net/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Aprender Azion Edge Functions", "completed": false}'
# Atualizar uma tarefa
curl -X PUT https://seu-dominio.map.azionedge.net/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Título da tarefa atualizada", "completed": true}'
# Excluir uma tarefa
curl -X DELETE https://seu-dominio.map.azionedge.net/tasks/1

Usando JavaScript fetch

// Obter todas as tarefas
const tasks = await fetch('/tasks').then(r => r.json());
// Criar uma nova tarefa
const newTask = await fetch('/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Nova tarefa',
completed: false
})
}).then(r => r.json());
// Atualizar uma tarefa
const updatedTask = await fetch('/tasks/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Tarefa atualizada',
completed: true
})
}).then(r => r.json());

Recursos avançados

Adicionando autenticação

Implemente autenticação para proteger sua API:

async function authenticateRequest(request) {
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Error('Autenticação necessária');
}
const token = authHeader.substring(7);
return await validateToken(token);
}

Validação de entrada

Adicione validação abrangente de entrada:

function validateTaskData(data) {
const errors = [];
if (!data.title || typeof data.title !== 'string') {
errors.push('Título é obrigatório e deve ser uma string');
}
if (data.title && data.title.length > 255) {
errors.push('Título deve ter menos de 255 caracteres');
}
if (data.completed !== undefined && typeof data.completed !== 'boolean') {
errors.push('Completed deve ser um boolean');
}
return errors;
}

Paginação

Implemente paginação para grandes conjuntos de dados:

async function getTasksPaginated(page = 1, limit = 10) {
const offset = (page - 1) * limit;
const query = `
SELECT * FROM tasks
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`;
const tasks = await database.query(query, [limit, offset]);
const countQuery = 'SELECT COUNT(*) as total FROM tasks';
const totalResult = await database.query(countQuery);
const total = totalResult.rows[0].total;
return {
tasks: tasks.rows,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
};
}

Solução de problemas

Problemas comuns e soluções

  • Erros de conexão com banco de dados: Verifique credenciais Edge SQL e string de conexão
  • Endpoint da API não encontrado: Verifique configuração de roteamento e deployment
  • Erros SQL: Valide schema do banco de dados e sintaxe de consulta
  • Problemas de performance: Otimize consultas SQL e indexação do banco de dados

Dicas de debugging

  1. Habilite registros: Adicione registros abrangentes para debugging
  2. Teste localmente: Use servidor de desenvolvimento local para isolar problemas
  3. Monitore performance: Rastreie tempos de resposta da API e consultas de banco de dados
  4. Tratamento de erros: Implemente respostas de erro adequadas e códigos de status

Próximos passos

  • Implemente filtragem e ordenação avançadas
  • Adicione atualizações em tempo real com WebSockets
  • Integre com provedores de autenticação
  • Implemente rate limiting e cache da API
  • Adicione documentação abrangente da API com OpenAPI/Swagger