Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ node_modules
.env.development.local
.env.test.local
.env.production.local
package-lock.json
package-lock.json
.vercel
97 changes: 91 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,98 @@
# Project Express API
# My Books API using Express.js

Replace this readme with your own information about your project.
In this project I created my first API using Express.js. It's a small API without a database that has a fixed amount of data about some books.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
## **Endpoints**

## The problem
### **1. List All Books**

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
**GET** `/api/books`

Retrieves all books in the "database". Supports filtering and sorting through query parameters.

| Query Parameter | Description | Example |
| --------------- | -------------------------------------------- | ------------------------------------ |
| `author` | Filter books by author name | `/api/books?author=Rowling` |
| `startsWith` | Filter books by title starting with a letter | `/api/books?startsWith=H` |
| `language` | Filter books by language code | `/api/books?language=eng` |
| `minRating` | Filter books by minimum average rating | `/api/books?minRating=4.5` |
| `sortBy` | Sort books by a field (e.g., `title`) | `/api/books?sortBy=title` |
| `order` | Sort order (`asc` or `desc`) | `/api/books?sortBy=title&order=desc` |

---

### **2. Get a Book by ID**

**GET** `/api/books/:id`

Retrieve a specific book by its unique `bookID`.

**Example**:

```plaintext
GET /api/books/1461
```

---

### **3. Add a New Book**

**POST** `/api/books`

Add a new book to the JSON file. The `bookID` is auto-generated. You must provide the following fields in the request body:

| Field | Type | Description |
| -------------------- | -------- | --------------------------- |
| `title` | `string` | Title of the book |
| `authors` | `string` | Author(s) of the book |
| `average_rating` | `number` | Average rating of the book |
| `isbn` | `number` | ISBN of the book |
| `isbn13` | `number` | ISBN-13 of the book |
| `language_code` | `string` | Language code of the book |
| `num_pages` | `number` | Number of pages in the book |
| `ratings_count` | `number` | Total ratings of the book |
| `text_reviews_count` | `number` | Number of text reviews |

**Example Request Body**:

```json
{
"title": "New Book Title",
"authors": "Author Name",
"average_rating": 4.7,
"isbn": 123456789,
"isbn13": 9781234567890,
"language_code": "eng",
"num_pages": 250,
"ratings_count": 100,
"text_reviews_count": 20
}
```

---

### **4. Filter Books**

Use query parameters to filter books based on criteria. Combine multiple filters as needed.

**Examples**:

- By author: `/api/books?author=Rowling`
- By title starting with "H": `/api/books?startsWith=H`
- By language: `/api/books?language=eng`
- By minimum rating: `/api/books?minRating=4.5`

---

### **5. Sort Books**

Use `sortBy` and `order` query parameters to sort books.

**Examples**:

- Sort by title (ascending): `/api/books?sortBy=title`
- Sort by rating (descending): `/api/books?sortBy=average_rating&order=desc`

## View it live

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
[Try it out for yourself »](https://project-express-api-ivory.vercel.app/)
7 changes: 7 additions & 0 deletions constants/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const ERRORS = {
BOOK_NOT_FOUND: "A book with this bookID does not exist.",
AUTHOR_NOT_FOUND: "No books found for the given author.",
DUPLICATE_BOOK: "Book with this bookID already exists.",
DUPLICATE_ISBN: "A book with this ISBN already exists.",
ADD_BOOK_ERROR: "An error occurred while adding the book.",
};
101 changes: 101 additions & 0 deletions controllers/bookController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import fs from "fs";
import path from "path";
import { ERRORS } from "../constants/constants";
import { generateID } from "../utils/utils";

// Get absolute path to the JSON file
const booksFilePath = path.resolve("data/books.json");

// Helper function to load data
const loadBooks = () => {
try {
const data = fs.readFileSync(booksFilePath, "utf-8");
return JSON.parse(data);
} catch (error) {
console.error("Error loading books:", error);
return [];
}
};

// Save books
const saveBooks = (books) => {
fs.writeFileSync(booksFilePath, JSON.stringify(books, null, 2));
};
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to change the books.json file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, Matilda! ☺️🙏
Yes, it writes to books.json and adds the new book at the end.


// Get all books
export const getBooks = (req, res) => {
const { author, startsWith, language, minRating, sortBy, order } = req.query;
let books = loadBooks();

// Apply all filters in a single loop
books = books.filter((book) => {
if (author && !book.authors.toLowerCase().includes(author.toLowerCase()))
return false;
if (
startsWith &&
book.title.charAt(0).toLowerCase() !== startsWith.toLowerCase()
)
return false;
if (language && book.language_code !== language) return false;
if (minRating && book.average_rating < parseFloat(minRating)) return false;
return true;
});

// Sort the results
if (sortBy) {
books.sort((a, b) => {
const orderFactor = order === "desc" ? -1 : 1;

if (sortBy === "average_rating") {
return (a.average_rating - b.average_rating) * orderFactor;
}

if (sortBy === "title") {
return a.title.localeCompare(b.title) * orderFactor;
}

return 0; // Default: no sorting if field not recognized
});
}

// Return all, filtered and/or sorted books
res.json(books);
};

// Get a specific book by ID
export const getBookByID = (req, res) => {
const books = loadBooks();
const id = req.params.id;

const book = books.find((book) => book.bookID === +id);

if (book) {
res.json(book);
} else {
return res.status(404).json({ error: ERRORS.BOOK_NOT_FOUND });
}
};

// Add a new book
export const addBook = (req, res) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, ambitious ⭐

try {
const newBook = req.body;
const books = loadBooks();

// Generate a unique ID
newBook.bookID = generateID(books);

// Ensure no duplicate ISBNs
if (books.some((book) => book.isbn === newBook.isbn)) {
return res.status(400).json({ error: ERRORS.DUPLICATE_ISBN });
}

books.push(newBook);
saveBooks(books);

res.status(201).json(newBook);
} catch (error) {
console.error("Error adding book:", error);
res.status(500).json({ error: ERRORS.ADD_BOOK_ERROR });
}
};
Loading