A simple REST API for managing a personal book library, built with Rust and Axum.
Base URL: https://book-library-api-production-f281.up.railway.app
- CRUD operations for books
- PostgreSQL persistence via sqlx
- Track availability across multiple physical copies of the same title
- Borrow and return individual copies with configurable loan periods
- List overdue borrowings
- User registration and login with Argon2 password hashing
- JWT-based authentication with role-based access control
adminrole required for mutating book data (add, update, delete, add copies)userrole required for borrowing, returning, and listing overdue borrowings
- Create a
.envfile (or export the variables) with your database URLs:
echo "DATABASE_URL=postgres://user:password@localhost/book_library" > .env
echo "JWT_SECRET=your-secret-key" >> .env
# For running tests, also set:
echo "TEST_DATABASE_URL=postgres://user:password@localhost/book_library_test" >> .env- Run database migrations:
cargo sqlx migrate run- Start the server:
cargo runThe server will start on http://localhost:3000
GET /health- Health checkGET /books- List all books (with optional filters and pagination)POST /books- Add a new book (requires admin JWT)GET /books/{id}- Get a book by IDPUT /books/{id}- Update a book (requires admin JWT)DELETE /books/{id}- Delete a book (requires admin JWT)
POST /books/{id}/copies- Add a physical copy of a book (requires admin JWT)
POST /register- Register a new user accountPOST /login- Log in and receive a JWT
POST /books/{id}/borrow- Borrow an available copy of a book (requires JWT)POST /books/{id}/return- Return a borrowed copy (requires JWT)GET /borrowings/overdue- List all overdue borrowings (requires JWT)
Add a book:
curl -X POST http://localhost:3000/books \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin-token>" \
-d '{
"title": "Clean Code",
"author": "Robert C. Martin",
"year": 2008,
"isbn": "978-0132350884"
}'A new book is created with one available copy. The response includes available_copies:
{
"id": 1,
"title": "Clean Code",
"author": "Robert C. Martin",
"year": 2008,
"isbn": "978-0132350884",
"available_copies": 1
}Add another physical copy of an existing book:
curl -X POST http://localhost:3000/books/1/copies \
-H "Authorization: Bearer <admin-token>"Returns 201 Created with the new copy record, or 404 if the book doesn't exist.
{
"id": 2,
"book_id": 1,
"available": true
}List all books:
curl http://localhost:3000/booksFilter books:
# Filter by availability (true = at least one copy available)
curl http://localhost:3000/books?available=true
# Filter by author (case-insensitive search)
curl http://localhost:3000/books?author=martin
# Filter by publication year
curl http://localhost:3000/books?year=2008
# Combine multiple filters
curl "http://localhost:3000/books?available=true&author=martin&year=2008"Paginate books:
# Get the second page with 5 books per page
curl "http://localhost:3000/books?page=2&limit=5"
# Combine pagination with filters
curl "http://localhost:3000/books?available=true&page=1&limit=20"The response includes a pagination metadata object alongside the data array:
{
"data": [...],
"pagination": {
"page": 1,
"limit": 10,
"total_items": 42,
"total_pages": 5
}
}
pagedefaults to1andlimitdefaults to10(max100).
Delete a book:
curl -X DELETE http://localhost:3000/books/1 \
-H "Authorization: Bearer <admin-token>"Deletes the book and all its associated copies.
Register a user:
curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "hunter2"}'Returns 201 Created with the new user record, or 409 Conflict if the username is already taken.
{
"id": 1,
"username": "alice"
}Log in:
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "hunter2"}'Returns 200 OK with a JWT valid for 7 days, or 401 Unauthorized on bad credentials.
{
"token": "eyJ0eXAiOi..."
}Borrow a book:
curl -X POST http://localhost:3000/books/1/borrow \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"borrower_name": "Alice", "days": 7}'
daysis optional and defaults to14. A validAuthorization: Bearer <token>header is required; omitting it returns401 Unauthorized. Any authenticated user (roleuseroradmin) may borrow a book.
Picks one available copy and marks it as borrowed. Returns 201 Created with the borrowing record (including the copy_id of the borrowed copy), or 404 if the book doesn't exist, or 409 Conflict if no copies are available.
Return a book:
curl -X POST http://localhost:3000/books/1/return \
-H "Authorization: Bearer <token>"Returns 200 OK and marks the copy as available again, or 400 Bad Request if no active borrowing is found for the book. A valid JWT is required.
List overdue borrowings:
curl http://localhost:3000/borrowings/overdue \
-H "Authorization: Bearer <token>"Returns all borrowings whose due_date has passed and that have not yet been returned:
[
{
"borrowing_id": 3,
"book_id": 1,
"book_title": "Clean Code",
"book_author": "Robert C. Martin",
"borrower_name": "Alice",
"borrowed_at": "2024-01-01T00:00:00+00:00",
"due_date": "2024-01-15T00:00:00+00:00"
}
]{
"id": 1,
"username": "alice",
"role": "user"
}Passwords are hashed with Argon2 and never returned in responses. The role field defaults to "user" and must be manually set to "admin" in the database to grant admin privileges.
{
"id": 1,
"title": "Book Title",
"author": "Author Name",
"year": 2024,
"isbn": "978-1234567890",
"available_copies": 2
}available_copies is the number of physical copies that are not currently borrowed.
{
"id": 1,
"book_id": 1,
"available": true
}{
"id": 1,
"copy_id": 1,
"borrower_name": "Alice",
"borrowed_at": "2024-01-01T00:00:00+00:00",
"due_date": "2024-01-15T00:00:00+00:00",
"returned_at": null
}When adding a new book, the following validations are enforced:
- Title: Must not be empty
- Author: Must not be empty
- Year: Must be between 1000 and the current year
- ISBN: Must be a valid ISBN-13 format (13 digits, hyphens allowed)
Invalid requests will return 400 Bad Request with an error message.
The project includes a comprehensive test suite covering all endpoints with both unit and integration tests.
Test coverage includes:
- All CRUD operations and their expected status codes
- Input validation (empty fields, invalid ISBN, future year)
- Filtering by author (case-insensitive), year, and availability
- Pagination correctness, limit capping, and out-of-bounds pages
- End-to-end integration flows (create → update → get, create → delete → 404, etc.)
- Borrow/return lifecycle (201 on borrow, 409 on no available copies, 200 on return, 400 on bad return)
- Overdue list filtering (excludes returned and future-due borrowings)
- Book copy management (add copy, 404 on unknown book, available_copies count increases)
- End-to-end borrow → return flow verifying
available_copiestransitions - User registration (201 on success, 409 on duplicate username)
- Login (200 + token on valid credentials, 401 on wrong password)
- Borrow authentication (401 when no token is provided)
- Admin-only endpoints return
403 Forbiddenwhen accessed with a non-admin token - Return and overdue list endpoints require authentication (401 when no token provided)
- Data is persisted in a PostgreSQL database specified by
DATABASE_URL. - A
JWT_SECRETenvironment variable must be set; it is used to sign and verify all tokens. - Tokens are signed with HS256 and expire after 7 days.
- Passwords are hashed with Argon2 (default parameters) before storage.
- Tests connect to a real PostgreSQL instance via
TEST_DATABASE_URLand reset state between runs usingTRUNCATE ... RESTART IDENTITY CASCADE. - Borrow and return operations use serializable transactions with
FOR UPDATE SKIP LOCKEDto safely handle concurrent requests against the same book. - The JWT payload includes a
roleclaim ("user"or"admin"). Admin-only routes extract and verify this claim server-side; presenting ausertoken to an admin route returns403 Forbidden.
MIT