API REST de catálogo de filmes. Importa filmes a partir de um arquivo CSV de forma assíncrona e expõe um endpoint de listagem com filtros e ordenação.
O desafio original pedia dois endpoints - um para importar um CSV e outro para listar os filmes. Esta implementação adiciona:
- Importação assíncrona via SolidQueue. O
POSTretorna202 Acceptedimediatamente com umimport_idque pode ser consultado depois. - Endpoint de status (
GET /api/v1/movie_imports/:id) para o cliente acompanhar a importação. - Stream do CSV com
CSV.foreach(headers: true)em vez de carregar o arquivo inteiro em memória. - Importação envelopada em transaction - linha inválida no meio do arquivo dispara rollback completo.
- Índices em
title(unique),year,genre,countryepublished_atpara os filtros Ransack. - Documentação OpenAPI gerada via rswag em
/api-docs.
- Ruby 3.3.5
- Rails 8.0
- PostgreSQL 16
- SolidQueue (job backend, mesma DB da aplicação)
- RSpec + rswag (testes + swagger)
- Docker Compose (web + worker + db)
Copiar .env.example para .env e ajustar o POSTGRES_PASSWORD:
cp .env.example .envBuildar a imagem:
docker compose buildSubir tudo (web + worker SolidQueue + Postgres):
docker compose up -dCriar e migrar o banco na primeira execução:
docker compose exec web bundle exec rails db:prepareDocumentação interativa via Swagger UI:
http://localhost:3001/api-docs
Entrar no container web:
docker compose exec web bashdocker compose exec web bundle exec rspecCobertura via SimpleCov é gerada em coverage/.
Recebe multipart/form-data com o arquivo CSV no campo file. O conteúdo é persistido em tmp/imports/, um MovieImport é criado com status processing e o ImportMoviesJob é enfileirado.
Request
POST /api/v1/movies
Content-Type: multipart/form-data
file=@netflix_titles.csvResponse - 202 Accepted
{
"message": "Importação aceita. Use o import_id para verificar o status.",
"import_id": 42
}Response - 400 Bad Request (arquivo ausente)
{
"error": "Arquivo não enviado. Por favor, anexe um arquivo CSV."
}Formato esperado do CSV:
show_id,type,title,director,cast,country,date_added,release_year,rating,duration,listed_in,description
s64,TV Show,13 Reasons Why,,"Dylan Minnette, ...",United States,"June 5, 2020",2020,TV-MA,4 Seasons,"Crime TV Shows, TV Dramas","After a teenage girl's ..."Retorna o estado de uma importação. O status evolui por processing → completed | failed | invalid_file.
Response - 200 OK
{
"id": 42,
"file_name": "netflix_titles.csv",
"error_message": null,
"status": "completed",
"movies_count": 131
}Response - 404 Not Found
{
"error": "Importação não encontrada."
}Possíveis estados de status:
| status | significado |
|---|---|
processing |
job ainda na fila ou em execução |
completed |
todos os registros foram inseridos |
failed |
algum registro violou validação - rollback aplicado, error_message preenchido |
invalid_file |
content-type não era text/csv ou o arquivo estava vazio |
Lista filmes ordenados por year asc por padrão, paginados via pagy.
Paginação
| query param | default | máximo | descrição |
|---|---|---|---|
page |
1 | - | número da página |
limit |
25 | 100 | tamanho da página |
Cabeçalhos de resposta:
| header | descrição |
|---|---|
Current-Page |
página atual |
Total-Pages |
total de páginas |
Total-Count |
total de registros |
Page-Limit |
tamanho da página efetivo |
Link |
RFC 5988 com rel="next", prev, first, last |
Exemplo:
GET /api/v1/movies?page=2&limit=10Response - 200 OK
[
{
"id": "b8ef4939-00b0-4f0f-b381-e2c4031d18cf",
"title": "A Clockwork Orange",
"genre": "Movie",
"year": 1971,
"country": "United Kingdom, United States",
"published_at": "2020-11-01",
"description": "In this dark satire from director Stanley Kubrick, a young, vicious sociopath in a dystopian England undergoes an experimental rehabilitation therapy."
}
]Quando nenhum filme é encontrado:
{ "message": "Nenhum filme encontrado" }Os filtros usam Ransack. Atributos permitidos: title, genre, year, country, published_at, description. Predicados Ransack (_eq, _cont, _gteq, …) são suportados.
Exemplo - filtro composto
GET /api/v1/movies?query[year_eq]=2020&query[country_eq]=Poland[
{
"id": "fe4292c8-9803-4561-bcb9-ab68796bf1a8",
"title": "365 Days",
"genre": "Movie",
"year": 2020,
"country": "Poland",
"published_at": "2020-06-07",
"description": "A fiery executive in a spiritless relationship falls victim to a dominant mafia boss, who imprisons her and gives her one year to fall in love with him."
}
]Ordenação customizada
GET /api/v1/movies?query[s]=title+ascFiltro inválido - 400 Bad Request
GET /api/v1/movies?query[yer_eq]=2020{ "error": "Parâmetro de busca inválido" }