This is the home of the Reading Room API, the back-end service of a full-stack personal project.
Repository for the project's React/Next.JS UI can be found here.
Reading Room is a full-stack web application that allows users to browse and catalogue a virtual library of books. New users can be registered, authenticated, and create custom "shelves"—individual collections of books. Books with corresponding user notes can then be edited or added/removed from each shelf. Within the "browse" section of the app, users can search for new books by title or author name. The app's target users are modern book-lovers who need an instant, mobile way to search for new books and keep track of their book collections.
The REST API is built with Java/Spring Boot, Docker, and PostgreSQL, and leverages the Open Library API for all book and author data. The service is deployed via Heroku and configured with unrestricted access for demoing purposes. See API Reference below for demoing the API with Postman. Instructions for registering/authenticating users via JSON Web Token, creating/updating/deleting shelves and books, and querying data are also outlined below, in addition to project setup instructions for running the application locally.
The project also includes a JUnit integration test suite for all repository classes, which leverages an H2 in-memory database.
Running this project and/or integration tests locally requires installations of JRE, JDK 17, Docker Desktop, and an IDE of your choice. Maven is also required and can either be installed locally, or accessed via IDE plugin.
- Clone this repository to your machine.
- Open Docker Desktop.
- From the command line, navigate to the top level of the project repository and run
docker-compose up
to start the Postgres database. - Open the project with your IDE and run the application to spin up the server.
Note that when running locally, the project is configured to seed all database tables with data for demo purposes. Stopping and starting the server will drop, create, and re-seed the tables.
All endpoints besides "User" endpoints (user log in and register) require a valid authentication token in the request header to recieve a successful response. To recieve an auth token cookie, use the /users/login
endpoint to log in as an existing user or create a new one using the /users/register
endpoint (User endpoints). Auth tokens are valid for 2 hours. Proper header formatting shown below (note the space between "Bearer" and token):
{ "Authorization": "Bearer <Auth Token>" }
To get started, login as an existing demo user or register a new user.
email:
jimmy@mail.com
password: guitar
The Open Library API refers to an author's individual works as "works," while treating individual editions of a work as "books." The current version of this application simply treats an author's individual works as "books" and does not expose data unique to any specific edition. As a result, the code for this project includes POJO interfaces for JSON deserialization that refer to works and books according to Open Library's semantics. This is important to keep in mind for understanding the source code of this project. For example, a JSON response from Open Library being recieved by this API may start as a "work," and once deserialized, be morphed into a "book" along with other data returned from a set of aggregated requests to Open Library.
Any book saved to a user's shelf includes a bookId
and libraryKey
field. A bookId
is unique to every saved book, while a libraryKey
is used for integrating with Open Library, and is used for fetching a book's details via the book details endpoint. For example, a user could save several "copies" of one book—all of which have the same libraryKey
field—that each have a unique bookId
.
DEPLOYED: https://reading-room-api-d84cba6ce967.herokuapp.com/api
LOCAL: http://localhost:8080/api
/users/register
Method | Request Body | Successful Response | Set-Cookie Header Value Example |
POST |
{
"firstName": string,
"lastName": string,
"email": string,
"password": string
}
|
{
"firstName": string,
"lastName": string,
"email": string
}
|
"token=eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOj; Path=/; Expires=Sat, 01 Jan 72000 08:00:00 GMT; Secure; HttpOnly" |
* Correct email format required.
/users/login
Method | Request Body | Successful Response | Set-Cookie Header Value Example |
POST |
{
"email": string,
"password": string
}
|
{
"firstName": string,
"lastName": string,
"email": string
}
|
"token=eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOj; Path=/; Expires=Sat, 01 Jan 72000 08:00:00 GMT; Secure; HttpOnly" |
/shelves
Method | Request Body | Successful Response |
GET |
n/a |
{
"shelfId": number,
"userId": number,
"title": string,
"description": string,
"totalSavedBooks": number
}[]
|
/shelves/{shelfId}
Method | Request Body | Successful Response |
GET |
n/a
|
{
"shelfId": number,
"userId": number,
"title": string,
"description": string,
"totalSavedBooks": number,
"books": {
"bookId": number,
"shelfId": number,
"userId": number,
"libraryKey": string,
"title": string,
"authors": {
name: string,
libraryKey: string
}[],
"coverUrl": string | null,
"userNote": string | null,
"savedDate": number
}[] | null
}
|
/shelves
Method | Request Body | Successful Response |
POST |
{
"title": string,
"description": string
}
|
{
"shelfId": number,
"userId": number,
"title": string,
"description": string,
"totalSavedBooks": number
}
|
/shelves/{shelfId}
Method | Request Body | Successful Response |
PUT |
{
"title": string,
"description": string
}
|
{ "success": boolean }
|
/shelves/{shelfId}
Method | Request Body | Successful Response |
DELETE |
n/a
|
{ "success": boolean }
|
/shelves/{shelfId}/books/{bookId}
Method | Request Body | Successful Response |
GET |
n/a
|
{
"bookId": number,
"shelfId": number,
"userId": number,
"libraryKey": string,
"title": string,
"authors": {
name: string,
libraryKey: string
}[],
"coverUrl": string | null,
"userNote": string | null,
"savedDate": number
}
|
/shelves/{shelfId}/books
Method | Request Body | Successful Response |
POST |
{
"libraryKey": string
}
|
{
"bookId": number,
"shelfId": number,
"userId": number,
"libraryKey": string,
"title": string,
"authors": {
name: string,
libraryKey: string
}[],
"coverUrl": string | null,
"userNote": string | null,
"savedDate": number
}
|
/shelves/{shelfId}/books/{bookId}
* Note: Only userNote
field can be updated.
Method | Request Body | Successful Response |
PUT |
{ "userNote": string | null }
|
{ "success": boolean }
|
/shelves/{shelfId}/books/{bookId}
Method | Request Body | Successful Response |
DELETE |
n/a
|
{ "success": boolean }
|
Note: query parameters in endpoints should replace whitespace with %20
/search/authors?q={authorName}&size={numberOfResultsPerPage}&page={pageNumber}
Method | Request Body | Successful Response |
GET |
n/a
|
{
"totalResults": number,
"pageSize": number,
"pageNum": number,
"results": {
"libraryKey": string,
"name": string,
"birthDate": string | null,
"deathDate": string | null,
"topBook": string | null,
"topSubjects": string[] | null
}[]
}
|
/search/authors/{authorLibraryKey}
Method | Request Body | Successful Response |
GET |
n/a
|
{
"libraryKey": string,
"name": string,
"bio": string | null,
"photoUrl": string | null,
"birthDate": string | null,
"deathDate": string | null,
"books": {
"libraryKey": string,
"title": string,
"publishDate": string,
"primaryAuthor": {
"name": string,
"libraryKey": string
},
"byMultipleAuthors": boolean,
"coverUrl": string | null,
"subjects": string[] | null
}[] | null,
}
|
/search/books?q={bookTitle}&size={numberOfResultsPerPage}&page={pageNumber}
Method | Request Body | Successful Response |
GET |
n/a
|
{
"totalResults": number,
"pageSize": number,
"pageNum": number,
"results: {
"libraryKey": string,
"title": string,
"publishYear": number | null,
"editionCount": number,
"authors": {
"name": string,
"libraryKey": string
}[],
"coverUrl": string | null,
"subjects": string[] | null
}[]
}
|
/search/books/{bookLibraryKey}
Method | Request Body | Successful Response |
GET |
n/a
|
{
"libraryKey": string,
"title": string,
"description": string | null,
"publishDate": string | null,
"authors": {
"name": string,
"libraryKey": string
}[]
"coverUrl": string | null,
"subjects": string[] | null,
"associatedShelves": {
"shelfId": number,
"title": string
}[]
}
|