DEV Community

Lucas Pereira de Souza
Lucas Pereira de Souza

Posted on

Tauri 2.0: Apps Desktop com Rust

logotech

## Desbravando o Cross-Platform: Apps Leves e Seguros com TypeScript e Node.js

No cenário atual de desenvolvimento de software, a necessidade de alcançar um público amplo em diversas plataformas (web, mobile, desktop) é cada vez mais premente. No entanto, a tentação de optar por abordagens nativas para cada plataforma pode levar a custos elevados, duplicação de esforços e, por vezes, a uma experiência de usuário inconsistente. É aqui que entram os aplicativos cross-platform, e o objetivo deste post é guiá-lo na criação de soluções que não apenas compartilham código entre plataformas, mas também se destacam pela leveza e segurança.

O Desafio do Cross-Platform: Mais que Compartilhar Código

Desenvolver para múltiplas plataformas pode parecer uma panaceia, mas esconde armadilhas. A busca por um único codebase frequentemente resulta em frameworks pesados, APIs de abstração complexas que mascaram peculiaridades de cada plataforma e, em muitos casos, brechas de segurança que se originam da própria arquitetura. Um aplicativo \"leve\" não se refere apenas ao tamanho do bundle final, mas também à sua performance em tempo de execução e à facilidade de manutenção. A segurança, por sua vez, não é um \"extra\", mas um pilar fundamental desde o início do projeto.

A Dupla Dinâmica: TypeScript e Node.js no Backend

Para construir aplicativos cross-platform robustos, escolhemos uma stack tecnológica que privilegia a clareza, a segurança e a performance:

  • TypeScript: Como um superset do JavaScript, o TypeScript adiciona tipagem estática opcional ao JavaScript. Isso significa que podemos detectar muitos erros em tempo de compilação, antes mesmo que o código chegue à produção. Para um backend, isso se traduz em código mais previsível, fácil de refatorar e menos propenso a bugs inesperados.
  • Node.js: Com seu modelo assíncrono e orientado a eventos, o Node.js é ideal para construir aplicativos de rede escaláveis. Sua vasta comunidade e ecossistema de pacotes (npm) oferecem soluções para praticamente qualquer necessidade.

Construindo um Backend Seguro e Leve com TypeScript e Node.js

Vamos mergulhar em exemplos práticos. Imagine que estamos construindo uma API simples para gerenciar usuários, com foco em segurança e boa arquitetura.

1. Estrutura do Projeto e Boas Práticas

Uma estrutura de projeto bem definida é o primeiro passo para um código organizado e de fácil manutenção.

// src/
// ├── config/         # Configurações da aplicação (ex: variáveis de ambiente)
// │   └── index.ts
// ├── controllers/    # Lógica de manipulação de requisições HTTP
// │   └── user.controller.ts
// ├── models/         # Definições de dados e interações com o banco de dados
// │   └── user.model.ts
// ├── routes/         # Definição das rotas da API
// │   └── user.routes.ts
// ├── services/       # Lógica de negócio
// │   └── user.service.ts
// ├── utils/          # Funções utilitárias
// │   └── logger.ts
// ├── app.ts          # Ponto de entrada da aplicação
// └── server.ts       # Configuração do servidor HTTP
Enter fullscreen mode Exit fullscreen mode

2. Tipagem Forte para Segurança e Clareza

Utilizar interfaces e tipos no TypeScript nos ajuda a garantir que os dados que entram e saem da nossa API estejam no formato esperado.

// src/models/user.model.ts

/**
 * Interface que define a estrutura de um usuário.
 * Garante que todos os usuários possuam as propriedades especificadas.
 */
export interface User {
  id: string;
  username: string;
  email: string;
  createdAt: Date;
}

/**
 * Interface para os dados de criação de um novo usuário.
 * Exclui o 'id' e 'createdAt', pois são gerados pelo servidor.
 */
export interface CreateUserDto {
  username: string;
  email: string;
}

/**
 * Interface para os dados de atualização de um usuário.
 * Todas as propriedades são opcionais para permitir atualizações parciais.
 */
export interface UpdateUserDto {
  username?: string;
  email?: string;
}
Enter fullscreen mode Exit fullscreen mode

3. Controllers: Manipulação Segura de Requisições

Os controllers são responsáveis por receber as requisições HTTP, validar os dados de entrada (usando DTOs) e chamar os serviços apropriados.

// src/controllers/user.controller.ts
import { Request, Response } from 'express';
import { UserService } from '../services/user.service';
import { CreateUserDto, UpdateUserDto, User } from '../models/user.model';
import { logger } from '../utils/logger';

export class UserController {
  constructor(private userService: UserService = new UserService()) {}

  /**
   * Cria um novo usuário.
   * Valida os dados de entrada usando CreateUserDto.
   * @param req - Objeto de requisição Express.
   * @param res - Objeto de resposta Express.
   */
  async createUser(req: Request, res: Response): Promise<void> {
    const userData: CreateUserDto = req.body;

    // Validação básica (em produção, usar bibliotecas como Zod ou class-validator)
    if (!userData.username || !userData.email) {
      logger.warn('Tentativa de criar usuário com dados incompletos.');
      res.status(400).json({ message: 'Username e email são obrigatórios.' });
      return;
    }

    try {
      const newUser: User = await this.userService.createUser(userData);
      logger.info(`Usuário criado com sucesso: ${newUser.username}`);
      res.status(201).json(newUser);
    } catch (error) {
      logger.error(`Erro ao criar usuário: ${error.message}`);
      res.status(500).json({ message: 'Erro interno do servidor ao criar usuário.' });
    }
  }

  /**
   * Obtém um usuário pelo ID.
   * @param req - Objeto de requisição Express.
   * @param res - Objeto de resposta Express.
   */
  async getUserById(req: Request, res: Response): Promise<void> {
    const { id } = req.params;

    try {
      const user: User | null = await this.userService.getUserById(id);
      if (user) {
        logger.info(`Usuário encontrado: ${user.username}`);
        res.status(200).json(user);
      } else {
        logger.warn(`Usuário com ID ${id} não encontrado.`);
        res.status(404).json({ message: 'Usuário não encontrado.' });
      }
    } catch (error) {
      logger.error(`Erro ao buscar usuário ${id}: ${error.message}`);
      res.status(500).json({ message: 'Erro interno do servidor ao buscar usuário.' });
    }
  }

  /**
   * Atualiza um usuário existente.
   * Valida os dados de entrada usando UpdateUserDto.
   * @param req - Objeto de requisição Express.
   * @param res - Objeto de resposta Express.
   */
  async updateUser(req: Request, res: Response): Promise<void> {
    const { id } = req.params;
    const updateData: UpdateUserDto = req.body;

    // Verifica se há dados para atualizar
    if (Object.keys(updateData).length === 0) {
      logger.warn(`Tentativa de atualizar usuário ${id} sem dados.`);
      res.status(400).json({ message: 'Nenhum dado fornecido para atualização.' });
      return;
    }

    try {
      const updatedUser: User | null = await this.userService.updateUser(id, updateData);
      if (updatedUser) {
        logger.info(`Usuário ${id} atualizado com sucesso.`);
        res.status(200).json(updatedUser);
      } else {
        logger.warn(`Usuário com ID ${id} não encontrado para atualização.`);
        res.status(404).json({ message: 'Usuário não encontrado.' });
      }
    } catch (error) {
      logger.error(`Erro ao atualizar usuário ${id}: ${error.message}`);
      res.status(500).json({ message: 'Erro interno do servidor ao atualizar usuário.' });
    }
  }

  /**
   * Deleta um usuário pelo ID.
   * @param req - Objeto de requisição Express.
   * @param res - Objeto de resposta Express.
   */
  async deleteUser(req: Request, res: Response): Promise<void> {
    const { id } = req.params;

    try {
      const success = await this.userService.deleteUser(id);
      if (success) {
        logger.info(`Usuário ${id} deletado com sucesso.`);
        res.status(204).send(); // 204 No Content é apropriado para deleções bem-sucedidas
      } else {
        logger.warn(`Usuário com ID ${id} não encontrado para deleção.`);
        res.status(404).json({ message: 'Usuário não encontrado.' });
      }
    } catch (error) {
      logger.error(`Erro ao deletar usuário ${id}: ${error.message}`);
      res.status(500).json({ message: 'Erro interno do servidor ao deletar usuário.' });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Services: Lógica de Negócio Centralizada e Segura

Os serviços contêm a lógica de negócio principal. Aqui, podemos implementar validações mais complexas e interações com o banco de dados. Para segurança, evitamos expor diretamente detalhes de implementação do banco de dados e focamos em fornecer uma API clara.

// src/services/user.service.ts
import { User, CreateUserDto, UpdateUserDto } from '../models/user.model';
import { v4 as uuidv4 } from 'uuid'; // Exemplo de uso de biblioteca UUID

// Simulação de um banco de dados em memória para este exemplo
const usersDatabase: Map<string, User> = new Map();

export class UserService {
  /**
   * Cria um novo usuário no \"banco de dados".
   * @param userData - Dados do usuário a ser criado.
   * @returns O usuário criado.
   */
  async createUser(userData: CreateUserDto): Promise<User> {
    const newUser: User = {
      id: uuidv4(), // Gera um ID único
      username: userData.username,
      email: userData.email,
      createdAt: new Date(),
    };
    usersDatabase.set(newUser.id, newUser);
    return newUser;
  }

  /**
   * Busca um usuário pelo seu ID.
   * @param id - O ID do usuário a ser buscado.
   * @returns O usuário encontrado ou null se não existir.
   */
  async getUserById(id: string): Promise<User | null> {
    const user = usersDatabase.get(id);
    return user || null;
  }

  /**
   * Atualiza um usuário existente.
   * @param id - O ID do usuário a ser atualizado.
   * @param updateData - Dados parciais para atualização.
   * @returns O usuário atualizado ou null se não for encontrado.
   */
  async updateUser(id: string, updateData: UpdateUserDto): Promise<User | null> {
    const existingUser = usersDatabase.get(id);
    if (!existingUser) {
      return null;
    }

    // Aplica as atualizações de forma segura
    const updatedUser = {
      ...existingUser,
      ...updateData,
      // Certifique-se de que campos como 'id' e 'createdAt' não sejam sobrescritos indevidamente
      id: existingUser.id,
      createdAt: existingUser.createdAt,
    };

    usersDatabase.set(id, updatedUser);
    return updatedUser;
  }

  /**
   * Deleta um usuário pelo seu ID.
   * @param id - O ID do usuário a ser deletado.
   * @returns true se a deleção foi bem-sucedida, false caso contrário.
   */
  async deleteUser(id: string): Promise<boolean> {
    return usersDatabase.delete(id);
  }
}

// Nota: Em um ambiente real, você usaria um ORM (como Prisma, TypeORM)
// ou um driver de banco de dados para interagir com um banco de dados persistente.
// A segurança de dados (criptografia, hashing de senhas, etc.) seria implementada aqui.
Enter fullscreen mode Exit fullscreen mode

5. Rotas: Orquestração das Requisições

As rotas definem os endpoints da API e associam cada endpoint a um método do controller.

// src/routes/user.routes.ts
import { Router } from 'express';
import { UserController } from '../controllers/user.controller';

const router = Router();
const userController = new UserController();

// POST /users - Criar novo usuário
router.post('/', userController.createUser.bind(userController));

// GET /users/:id - Obter usuário por ID
router.get('/:id', userController.getUserById.bind(userController));

// PUT /users/:id - Atualizar usuário
router.put('/:id', userController.updateUser.bind(userController));

// DELETE /users/:id - Deletar usuário
router.delete('/:id', userController.deleteUser.bind(userController));

export default router;
Enter fullscreen mode Exit fullscreen mode

6. Ponto de Entrada e Servidor

O app.ts configura o middleware e as rotas, enquanto server.ts inicia o servidor HTTP.

// src/app.ts
import express, { Express, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet'; // Ajuda a proteger contra vulnerabilidades web comuns
import dotenv from 'dotenv';
import userRoutes from './routes/user.routes';
import { logger } from './utils/logger';

dotenv.config(); // Carrega variáveis de ambiente do arquivo .env

const app: Express = express();

// Middlewares de Segurança
app.use(cors()); // Configurar opções de CORS conforme necessário para produção
app.use(helmet()); // Adiciona headers de segurança HTTP

// Middlewares de Parsing
app.use(express.json()); // Para parsear JSON bodies
app.use(express.urlencoded({ extended: true })); // Para parsear URL-encoded bodies

// Rota principal da API de usuários
app.use('/api/v1/users', userRoutes);

// Middleware de tratamento de erros genérico
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  logger.error(`Erro não tratado: ${err.message}`);
  res.status(500).json({ message: 'Ocorreu um erro inesperado no servidor.' });
});

export default app;

// src/server.ts
import app from './app';
import { logger } from './utils/logger';

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  logger.info(`Servidor rodando na porta ${PORT}`);
});

// src/utils/logger.ts
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    // Em produção, você adicionaria transportes para arquivos ou serviços de log
    // new winston.transports.File({ filename: 'error.log', level: 'error' }),
    // new winston.transports.File({ filename: 'combined.log' }),
  ],
});

export { logger };
Enter fullscreen mode Exit fullscreen mode

Considerações Adicionais para Leveza e Segurança

  • Gerenciamento de Dependências: Mantenha suas dependências atualizadas e use ferramentas como npm audit para identificar vulnerabilidades. Remova pacotes não utilizados.
  • Autenticação e Autorização: Implemente mecanismos robustos como JWT (JSON Web Tokens) ou OAuth2. No backend, sempre valide tokens e verifique permissões antes de executar ações sensíveis.
  • Validação de Entrada: Nunca confie nos dados vindos do cliente. Use bibliotecas dedicadas (como zod, class-validator) para validar rigorosamente todos os inputs.
  • Tratamento de Erros: Implemente um sistema de tratamento de erros centralizado e robusto. Evite expor informações sensíveis de erros para o cliente.
  • Rate Limiting: Proteja sua API contra ataques de força bruta e abuso, limitando o número de requisições que um cliente pode fazer em um determinado período.
  • HTTPS: Sempre use HTTPS para criptografar a comunicação entre o cliente e o servidor.
  • Segurança de Dados: Para dados sensíveis (senhas, informações pessoais), utilize hashing (ex: bcrypt) e criptografia apropriada.

Conclusão: O Caminho para Aplicações Cross-Platform de Sucesso

Criar aplicativos cross-platform leves e seguros não é um objetivo inatingível, mas sim o resultado de escolhas arquitetônicas conscientes e da adoção de boas práticas. Ao alavancar o poder do TypeScript para tipagem forte e a eficiência do Node.js para o backend, construímos uma base sólida. A atenção contínua à segurança, desde a validação de dados até a autenticação robusta, garante que suas aplicações não apenas alcancem mais usuários, mas também o façam de forma confiável e protegida. Lembre-se: a jornada para um código de qualidade é contínua, e a segurança deve ser uma prioridade em cada passo do desenvolvimento.

Top comments (0)