Backend exercise built with Symfony 7.4. Focus areas:
- Domain-first design (Book, BookId, Person) with validation and immutability.
- Application layer with contracts and use-case handlers (search, get by id).
- Infrastructure adapters for Gutendex (HTTP client + mapper) and HTTP controller/transformer.
- PHP 8.2+
- Composer
- MySQL (if you need DB locally; current flow uses Gutendex API)
- Symfony CLI optional (for local server tooling)
cd server
composer install
cp .env .env.local # adjust APP_SECRET and GUTENDEX_BASE_URL if neededcd server
symfony serve -d # or php -S localhost:8000 -t publiccd docker/excercise
export USER_ID=$(id -u) USER_NAME=$(id -un)
export DB_DATABASE=exercise DB_USERNAME=exercise DB_PASSWORD=exercise
export NGINX_ENV=local
docker compose -f docker-compose.yml up -d --buildServices/ports:
- App/Nginx: http://127.0.0.1:8086
- MySQL: 127.0.0.1:35076 (user/pass from env above)
Redis service is optional; keep it running only if you want response caching for Gutendex calls.
Code is mounted from server/ into the container; edits on host are reflected live.
GET /books?search=<query>: searches books via Gutendex, returns an array of books.GET /books/{id}: returns a single book by Gutenberg id.
- Swagger UI:
https://exercise.local/api/doc - OpenAPI JSON:
https://exercise.local/api/doc.json - Example calls (self-signed, use
-k): - Base URL is configurable via
API_BASE_URL(default: https://exercise.local).curl -k "https://exercise.local/books?search=Frankenstein"curl -k "https://exercise.local/books/1342"
{
"id": 1342,
"title": "Pride and Prejudice",
"subjects": ["Love", "Society"],
"authors": [
{"name": "Jane Austen", "birth_year": 1775, "death_year": 1817}
]
}- 400
{ "error": "invalid_id" } - 404
{ "error": "book_not_found" } - 503
{ "error": "upstream_unavailable" }(Gutendex/network issue) - 500
{ "error": "internal_error" }
- Start the server (see Run the app).
- Search:
curl "http://127.0.0.1:8086/books?search=Frankenstein"(use 8000 if running symfony serve). - Detail:
curl "http://127.0.0.1:8086/books/1342" - Not found:
curl "http://127.0.0.1:8086/books/999999"→ 404.
cd server
php bin/phpunit- Cache check:
php bin/console app:redis:checkwrites/reads a test item in the Redis-backed book catalog cache and lists sample keys.
GUTENDEX_BASE_URL(default: https://gutendex.com)APP_ENV,APP_SECRETas usual for Symfony.
Purpose
- Cache Gutendex responses to speed up repeated searches and book lookups. Domain/Application remain untouched; a decorator wraps the Gutendex adapter.
Enable/disable
- Toggle:
BOOK_CATALOG_CACHE_ENABLED(1on,0off). When off, calls bypass cache.
Configuration
REDIS_DSN(default:redis://redis:6379) — only required when cache is enabled.BOOK_CATALOG_CACHE_TTL(default:300seconds); negative lookups cache for60seconds.
Keys
- Namespaced keys ending with
books.search.{sha1(normalized query)}andbooks.id.{id}(namespace added by Symfony cache).
How to verify
- Make the same request twice and observe
X-BookCatalog-Cacheheader (MISSthenHIT). - Run the diagnostic command (Redis running):
php bin/console app:redis:checkto write/read a probe item and list sample keys.
This project exposes an OpenAPI specification generated by NelmioApiDocBundle.
- Swagger UI (interactive docs):
GET /api/doc - OpenAPI JSON (raw spec):
GET /api/doc.json
- Open
https://exercise.local/api/docin your browser to explore and try the endpoints. - Use
https://exercise.local/api/doc.jsonto download/consume the OpenAPI spec (e.g., Postman, Swagger tools, CI validation).
- The JSON spec is generated dynamically from the application routes and API configuration.
- If you change routes or API annotations/attributes, clear the Symfony cache to refresh the output.