Frontend SWE — TypeScript / React / Next.js
Princípios
- TypeScript strict:
strict: true, sem any, preferir unknown + narrowing.
- Separation of Concerns: UI (components) ≠ data fetching (hooks/services) ≠ tipos (types/).
- Server-first (Next.js): Server Components por default; Client Components só com interatividade.
- Acessibilidade: semantic HTML, ARIA quando necessário, keyboard navigation.
- Performance: lazy load, memoização consciente (não prematura).
Estrutura de Projeto (Next.js App Router)
src/
├── app/ # Rotas (App Router)
│ ├── layout.tsx
│ ├── page.tsx
│ └── orders/
│ └── page.tsx
├── components/
│ ├── ui/ # Primitivos reutilizáveis (Button, Input)
│ └── features/ # Componentes de domínio (OrderList)
├── hooks/ # Custom hooks (useOrders)
├── lib/
│ ├── api/ # Clients HTTP tipados
│ └── utils/
├── types/ # Tipos compartilhados
└── tests/
├── unit/
└── integration/
TypeScript — Padrões
// types/order.ts
export type OrderStatus = 'pending' | 'confirmed' | 'cancelled';
export interface Order {
readonly id: string;
readonly customerId: string;
readonly status: OrderStatus;
readonly total: Money;
}
export interface Money {
readonly amount: string; // Decimal como string
readonly currency: string;
}
// Discriminated union para estados de UI
export type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
Server vs Client Components
// app/orders/page.tsx — Server Component (default)
import { fetchOrders } from '@/lib/api/orders';
export default async function OrdersPage() {
const orders = await fetchOrders(); // fetch no servidor
return <OrderList orders={orders} />;
}
// components/features/OrderActions.tsx — Client Component
'use client';
import { useState } from 'react';
export function OrderActions({ orderId }: { orderId: string }) {
const [loading, setLoading] = useState(false);
// interatividade, event handlers
}
Data Fetching
// lib/api/orders.ts
const API_BASE = process.env.NEXT_PUBLIC_API_URL!;
export async function fetchOrders(): Promise<Order[]> {
const res = await fetch(`${API_BASE}/api/v1/orders`, {
next: { revalidate: 60 }, // ISR
headers: { Authorization: `Bearer ${await getToken()}` },
});
if (!res.ok) throw new ApiError(res.status, await res.text());
return res.json();
}
Testes — Vitest + Testing Library
// tests/unit/components/OrderList.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { OrderList } from '@/components/features/OrderList';
describe('OrderList', () => {
it('should_display_orders_when_data_provided', () => {
render(<OrderList orders={[buildOrder({ id: '1', status: 'pending' })]} />);
expect(screen.getByText(/pending/i)).toBeInTheDocument();
});
it('should_call_onCancel_when_cancel_clicked', async () => {
const
const user = userEvent.setup();
render(<OrderList orders={[buildOrder()]} />);
await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(onCancel).toHaveBeenCalledOnce();
});
});
// MSW para mock de API
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/v1/orders', () => HttpResponse.json([{ id: '1', status: 'pending' }]))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Observabilidade Frontend
// Sentry
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.NODE_ENV,
});
// Web Vitals
export function reportWebVitals(metric: NextWebVitalsMetric) {
// enviar para analytics
console.log(metric.name, metric.value);
}
Segurança
// next.config.ts — CSP
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';",
},
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
];
// Sanitizar HTML de usuário
import DOMPurify from 'isomorphic-dompurify';
const safeHtml = DOMPurify.sanitize(userHtml);
Anti-Patterns
| Anti-Pattern |
Problema |
any em todo lugar |
Perde type safety |
useEffect para fetch que Server Component resolve |
Complexidade desnecessária |
| Estado global para tudo |
Re-renders, acoplamento |
| Inline styles massivos |
Manutenção difícil |
| Sem testes de interação |
Regressões em UX |
Checklist Frontend