REST JSON CORS
Redes & HTTP

REST & JSON

REST não é uma tecnologia nem uma biblioteca — é um conjunto de constraints arquitecturais que, quando aplicadas ao HTTP, produzem APIs previsíveis, escaláveis, e fáceis de consumir. JSON é o formato de serialização que se tornou o standard de facto para a troca de dados em APIs web. Juntos, definem o contrato entre o servidor Spring Boot e qualquer cliente — browser, aplicação móvel, ou outro serviço.

O Que é REST

REST (Representational State Transfer) foi definido por Roy Fielding na sua dissertação de doutoramento em 2000, descrevendo as constraints arquitecturais que tornaram a Web escalável. Não é um protocolo nem um standard formal — é um estilo arquitectural. Uma API que segue estas constraints chama-se RESTful.

ConstraintO que significaImplicação prática
Cliente-Servidor Separação clara entre quem pede e quem responde O frontend e o backend evoluem independentemente. O Spring Boot não sabe nada sobre React ou Angular.
Stateless Cada pedido contém toda a informação necessária para ser processado O servidor não guarda estado de sessão entre pedidos. O JWT no cookie garante isto — cada pedido é autónomo.
Cacheable As respostas indicam se podem ser guardadas em cache Headers Cache-Control, ETag. Respostas de GET estáticas podem ser cacheadas por CDNs.
Interface Uniforme Recursos identificados por URLs; manipulados pelos métodos HTTP standard O mesmo padrão para todas as entidades: GET/POST/PUT/DELETE sobre /api/recurso.
Sistema em Camadas O cliente não sabe se fala directamente com o servidor ou com um intermediário Permite load balancers, proxies, CDNs e gateways entre cliente e servidor — transparentes para a aplicação.
A constraint Stateless e o JWT A constraint mais importante para a arquitectura deste projecto é o Stateless. O servidor não guarda sessões em memória — cada pedido tem de provar a sua identidade por si próprio. O JWT no HttpOnly cookie é exactamente isso: um token assinado criptograficamente que o cliente envia em cada pedido, e que o servidor valida sem consultar nenhuma base de dados de sessões. Horizontal scaling — múltiplas instâncias do servidor — funciona porque nenhuma instância tem estado que as outras não têm.

Recursos vs Acções: o Design de URLs

O erro mais comum no design de APIs REST é usar URLs como acções — colocar verbos no path. REST usa os métodos HTTP como verbos e os URLs como substantivos que identificam recursos. O URL identifica o quê; o método HTTP diz o quê fazer.

// ❌ API RPC-style — verbos nos URLs
POST /api/getAccount?id=42
POST /api/createAccount
POST /api/deleteAccount?id=42
POST /api/suspendAccount?id=42
POST /api/listUserAccounts?userId=99

// ✅ API RESTful — recursos nos URLs, verbos nos métodos HTTP

// Colecção de contas
GET    /api/accounts          → listar as contas do utilizador autenticado
POST   /api/accounts          → criar uma nova conta

// Recurso individual
GET    /api/accounts/{id}     → ler a conta {id}
PUT    /api/accounts/{id}     → substituir a conta {id} completamente
PATCH  /api/accounts/{id}     → actualizar campos específicos da conta {id}
DELETE /api/accounts/{id}     → eliminar a conta {id}

// Recursos aninhados — relações entre recursos
GET    /api/accounts/{id}/transactions        → transacções da conta {id}
POST   /api/accounts/{id}/transactions        → criar transacção na conta {id}
GET    /api/accounts/{id}/transactions/{tid}  → transacção específica

// Acções que não mapeiam naturalmente para CRUD — usar sub-recursos substantivos
POST   /api/accounts/{id}/suspension          → suspender conta (cria o estado "suspension")
DELETE /api/accounts/{id}/suspension          → reactivar conta (remove o estado "suspension")
POST   /api/accounts/{id}/transfers           → iniciar transferência (cria um recurso "transfer")
// Implementação em Spring Boot — @RestController com métodos HTTP correctos

@RestController
@RequestMapping("/api/accounts")
public class AccountController {
    private final AccountService service;

    public AccountController(AccountService service) { this.service = service; }

    // GET /api/accounts — listar contas do utilizador autenticado
    @GetMapping
    public List<AccountResponse> list(@AuthenticationPrincipal UserDetails user) {
        Long userId = ((AppUserDetails) user).getId();
        return service.listAccounts(userId);
    }

    // POST /api/accounts — criar conta
    @PostMapping
    public ResponseEntity<AccountResponse> create(
            @Valid @RequestBody CreateAccountRequest request,
            @AuthenticationPrincipal UserDetails user) {
        Long userId = ((AppUserDetails) user).getId();
        AccountResponse response = service.createAccount(request, userId);
        URI location = URI.create("/api/accounts/" + response.id());
        return ResponseEntity.created(location).body(response);
        //                    ↑ 201 Created com header Location
    }

    // GET /api/accounts/{id}
    @GetMapping("/{id}")
    public AccountResponse get(@PathVariable Long id,
                                @AuthenticationPrincipal UserDetails user) {
        return service.getAccount(id, ((AppUserDetails) user).getId());
    }

    // PATCH /api/accounts/{id} — actualização parcial
    @PatchMapping("/{id}")
    public AccountResponse update(@PathVariable Long id,
                                   @Valid @RequestBody UpdateAccountRequest request,
                                   @AuthenticationPrincipal UserDetails user) {
        return service.updateAccount(id, request, ((AppUserDetails) user).getId());
    }

    // DELETE /api/accounts/{id}
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id,
                                        @AuthenticationPrincipal UserDetails user) {
        service.deleteAccount(id, ((AppUserDetails) user).getId());
        return ResponseEntity.noContent().build(); // 204 No Content
    }
}

JSON: Serialização e Deserialização

JSON (JavaScript Object Notation) é um formato de texto para representar estruturas de dados. Em Spring Boot, o Jackson é a biblioteca que converte automaticamente objectos Java em JSON (serialização) e JSON em objectos Java (deserialização) — sem código manual.

// Java → JSON (serialização): quando o controller retorna um objecto
// Jackson converte automaticamente via @ResponseBody (incluído em @RestController)

// Java:
AccountResponse response = new AccountResponse(42L, "Ana Silva", 1500.0, AccountType.SAVINGS);

// JSON resultante (no body da resposta HTTP):
{
  "id": 42,
  "ownerName": "Ana Silva",
  "balance": 1500.0,
  "type": "SAVINGS"
}

// JSON → Java (deserialização): quando o controller recebe um @RequestBody
// Jackson lê o JSON do body do pedido e preenche o record/classe

// JSON recebido:
{"ownerName": "Ana Silva", "initialBalance": 1500.0, "type": "SAVINGS"}

// Java resultante:
CreateAccountRequest request = new CreateAccountRequest("Ana Silva", 1500.0, AccountType.SAVINGS);
// Customização do Jackson — anotações úteis nos DTOs

public record AccountResponse(
    Long id,

    // @JsonProperty — renomear o campo no JSON
    @JsonProperty("owner_name")
    String ownerName,          // Java: ownerName → JSON: "owner_name"

    double balance,

    AccountType type,

    // @JsonIgnore — excluir o campo do JSON (alternativa ao DTO separado)
    @JsonIgnore
    Long internalCode,         // nunca aparece no JSON de resposta

    // @JsonFormat — formatar datas
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
    Instant createdAt
) {}

// Configuração global do Jackson no application.properties:
spring.jackson.property-naming-strategy=SNAKE_CASE   // camelCase → snake_case automaticamente
spring.jackson.serialization.write-dates-as-timestamps=false  // datas como ISO 8601, não epoch
spring.jackson.deserialization.fail-on-unknown-properties=false  // ignorar campos desconhecidos no input
Nunca serializar entidades JPA directamente Serializar uma entidade JPA com Jackson pode causar loops infinitos em relações bidirecionais (Account → User → accounts → Account → ...), expor campos internos como passwordHash e ownerId, e disparar queries lazy não intencionais. Usa sempre DTOs de resposta (ver Módulo Java OOP) — a entidade nunca deve sair da camada de serviço.

CORS: Pedidos Cross-Origin

O browser implementa a Same-Origin Policy — por omissão, uma página carregada de https://app.randomt.pt não pode fazer pedidos fetch/XHR para https://api.randomt.pt porque as origens são diferentes (subdomínio diferente). CORS (Cross-Origin Resource Sharing) é o mecanismo que permite ao servidor autorizar explicitamente pedidos de origens específicas.

// O que define uma "origem":
// protocolo + domínio + porto

// Mesma origem (permitido sem CORS):
https://app.randomt.pt/login  →  https://app.randomt.pt/api/auth/login  ✅

// Origens diferentes (bloqueado sem CORS):
https://app.randomt.pt        →  https://api.randomt.pt         ❌ subdomínio diferente
https://app.randomt.pt        →  http://app.randomt.pt          ❌ protocolo diferente
https://app.randomt.pt        →  https://app.randomt.pt:8080    ❌ porto diferente

// O preflight — pedido OPTIONS automático do browser
// Antes de um pedido POST/PUT/PATCH/DELETE cross-origin, o browser pergunta:
// "Servidor, posso fazer este pedido da origem https://app.randomt.pt?"

OPTIONS /api/accounts HTTP/1.1
Host: api.randomt.pt
Origin: https://app.randomt.pt
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

// O servidor responde se autoriza:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.randomt.pt
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true   // necessário para cookies cross-origin
Access-Control-Max-Age: 3600             // cache do preflight por 1 hora

// Só depois o browser envia o pedido real.
// Configuração CORS em Spring Boot

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        // origens autorizadas — explícitas, nunca "*" com credentials
        config.setAllowedOrigins(List.of(
            "https://app.randomt.pt",
            "https://www.randomt.pt"
        ));

        config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Content-Type", "Accept"));

        // necessário para que o browser envie cookies em pedidos cross-origin
        // sem isto, o Cookie: access_token=... não é incluído
        config.setAllowCredentials(true);

        // cache do preflight — o browser não repete o OPTIONS durante 1 hora
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

// Registar na SecurityFilterChain:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
        CorsConfigurationSource corsConfigurationSource) throws Exception {
    return http
        .cors(cors -> cors.configurationSource(corsConfigurationSource))
        // ... resto da configuração
        .build();
}
Nunca usar Access-Control-Allow-Origin: * com cookies O wildcard * e Access-Control-Allow-Credentials: true são mutuamente exclusivos — o browser rejeita esta combinação. Mais importante: * permite que qualquer origem no mundo faça pedidos autenticados à tua API. Define sempre origens explícitas em produção. Em desenvolvimento, usa http://localhost:3000 — nunca * em produção.

Versionamento de API

Uma API pública é um contrato com os seus clientes. Quando o contrato muda de forma incompatível — campos removidos, tipos alterados, comportamento diferente — os clientes existentes quebram. O versionamento permite evoluir a API sem quebrar clientes antigos.

// Estratégias de versionamento — a mais comum em APIs REST

// 1. Versão no path (mais explícita e recomendada)
GET /api/v1/accounts/42
GET /api/v2/accounts/42   // nova versão com contrato diferente

// Em Spring Boot:
@RestController
@RequestMapping("/api/v1/accounts")
public class AccountControllerV1 { ... }

@RestController
@RequestMapping("/api/v2/accounts")
public class AccountControllerV2 { ... }

// 2. Versão no header Accept (mais "purista" REST, menos prático)
GET /api/accounts/42
Accept: application/vnd.randomt.v2+json

// 3. Versão em query parameter (evitar — polui os URLs e dificulta caching)
GET /api/accounts/42?version=2

// Regra prática: versão no path é a mais legível,
// fácil de documentar, e fácil de testar no browser.

Paginação e Filtros

Uma API que retorna todos os registos de uma tabela num único pedido é uma API que vai falhar em produção. Paginação e filtros são contratos que o cliente e o servidor estabelecem via query parameters.

// Query parameters para paginação e filtros

GET /api/accounts?page=0&size=20&sort=createdAt,desc
GET /api/transactions?accountId=42&from=2026-01-01&to=2026-06-01&type=DEBIT

// Spring Data JPA integra com Pageable automaticamente
@GetMapping
public Page<AccountResponse> list(
        @AuthenticationPrincipal UserDetails user,
        @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC)
        Pageable pageable) {

    Long userId = ((AppUserDetails) user).getId();
    return service.listAccounts(userId, pageable);
}

// Resposta paginada — estrutura standard do Spring Data:
{
  "content": [
    {"id": 43, "ownerName": "Ana Silva", ...},
    {"id": 41, "ownerName": "Ana Silva", ...}
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 20,
    "sort": {"sorted": true, "property": "createdAt", "direction": "DESC"}
  },
  "totalElements": 3,
  "totalPages": 1,
  "last": true,
  "first": true
}

// Nota de segurança: o userId vem sempre do JWT — nunca do query parameter
// GET /api/accounts?userId=99  ← o userId do query parameter é IGNORADO
// o servidor usa sempre o userId do token autenticado

Documentação com OpenAPI / Swagger

Uma API sem documentação é uma API difícil de usar e difícil de auditar. O SpringDoc gera documentação OpenAPI 3.0 automaticamente a partir das anotações do Spring Boot — sem escrita manual.

<!-- pom.xml — adicionar SpringDoc -->
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.5.0</version>
</dependency>
// Anotações de documentação nos controllers
@RestController
@RequestMapping("/api/accounts")
@Tag(name = "Accounts", description = "Gestão de contas do utilizador autenticado")
public class AccountController {

    @Operation(
        summary = "Obter conta por ID",
        description = "Retorna a conta se pertencer ao utilizador autenticado. " +
                      "Retorna 404 se não existir ou pertencer a outro utilizador."
    )
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "Conta encontrada"),
        @ApiResponse(responseCode = "401", description = "Não autenticado"),
        @ApiResponse(responseCode = "404", description = "Conta não encontrada")
        // nota: 403 não está na lista — usamos 404 para recursos alheios (anti-IDOR)
    })
    @GetMapping("/{id}")
    public AccountResponse get(@PathVariable Long id,
                                @AuthenticationPrincipal UserDetails user) {
        return service.getAccount(id, ((AppUserDetails) user).getId());
    }
}

// A documentação fica disponível em:
// http://localhost:8080/swagger-ui.html     — UI interactiva
// http://localhost:8080/api-docs            — JSON OpenAPI 3.0

// Proteger o Swagger em produção:
// application-prod.properties:
springdoc.swagger-ui.enabled=false   // desactivar UI em produção
springdoc.api-docs.enabled=false     // desactivar JSON spec em produção

O NAV Actualizado — Redes & HTTP

Com os três artigos desta série completos, o NAV do shell.js deve reflectir o novo grupo.

// Adicionar ao NAV em shell.js:
{
  group: 'Redes & HTTP',
  items: [
    { key: 'http',      label: 'Como a Internet Funciona',  href: 'http-overview.html' },
    { key: 'httprr',    label: 'Request & Response',        href: 'http-request-response.html' },
    { key: 'rest',      label: 'REST & JSON',              href: 'http-rest-json.html' }
  ]
}

Checklist REST & JSON