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.
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.
| Constraint | O que significa | Implicaçã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. |
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 (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
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.
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();
}
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.
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.
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
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
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' }
]
}
Location em criações via POST.ownerId e campos internos nunca aparecem no JSON.* com allowCredentials: true.allowCredentials: true obrigatório para que o browser envie cookies em pedidos cross-origin.userId vem sempre do JWT autenticado — nunca de query parameters ou body do pedido.