Skip to content

tanciaku/book-library-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Book Library API

A simple REST API for managing a personal book library, built with Rust and Axum.

Live Demo

Base URL: https://book-library-api-production-f281.up.railway.app

Features

  • 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
  • admin role required for mutating book data (add, update, delete, add copies)
  • user role required for borrowing, returning, and listing overdue borrowings

Quick Start

  1. Create a .env file (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
  1. Run database migrations:
cargo sqlx migrate run
  1. Start the server:
cargo run

The server will start on http://localhost:3000

API Endpoints

Books

  • GET /health - Health check
  • GET /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 ID
  • PUT /books/{id} - Update a book (requires admin JWT)
  • DELETE /books/{id} - Delete a book (requires admin JWT)

Book Copies

  • POST /books/{id}/copies - Add a physical copy of a book (requires admin JWT)

Auth

  • POST /register - Register a new user account
  • POST /login - Log in and receive a JWT

Borrowings

  • 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)

Example Requests

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/books

Filter 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
  }
}

page defaults to 1 and limit defaults to 10 (max 100).

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}'

days is optional and defaults to 14. A valid Authorization: Bearer <token> header is required; omitting it returns 401 Unauthorized. Any authenticated user (role user or admin) 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"
  }
]

Data Model

User

{
  "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.

Book

{
  "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.

Book Copy

{
  "id": 1,
  "book_id": 1,
  "available": true
}

Borrowing

{
  "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
}

Validation

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.

Testing

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_copies transitions
  • 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 Forbidden when accessed with a non-admin token
  • Return and overdue list endpoints require authentication (401 when no token provided)

Notes

  • Data is persisted in a PostgreSQL database specified by DATABASE_URL.
  • A JWT_SECRET environment 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_URL and reset state between runs using TRUNCATE ... RESTART IDENTITY CASCADE.
  • Borrow and return operations use serializable transactions with FOR UPDATE SKIP LOCKED to safely handle concurrent requests against the same book.
  • The JWT payload includes a role claim ("user" or "admin"). Admin-only routes extract and verify this claim server-side; presenting a user token to an admin route returns 403 Forbidden.

License

MIT

About

REST API for managing a personal book library — built with Rust, Axum, and PostgreSQL.

Resources

License

Stars

Watchers

Forks

Contributors

Languages