In this article, we'll develop a complete Library Management System using Harper, demonstrating how this modern platform makes building robust APIs straightforward and efficient.
Understanding Our Library Management System
Before diving into the code, let's understand what our system will accomplish. We're building a Library Management System with two primary components:
- Book Management - Creating, retrieving, updating, and deleting books in our library catalog
- Member Management - Managing library patrons with similar CRUD (Create, Read, Update, Delete) operations
Each book in our system will have properties like title, author, and availability status. Similarly, members will have personal information and a record of borrowed books. This fundamental structure allows for tracking both the library's inventory and its members' borrowing activities.
While a production system might include additional features like reservations, late fees, or categorization systems, our implementation focuses on the core functionality that demonstrates Harper's capabilities. The patterns established here can easily be extended for more complex requirements.
Getting Started with Harper
If you haven't installed Harper yet, refer to our previous article on Installing Harper where we covered three installation methods:
- Global installation via npm
- Running Harper in Docker
- Offline installation for disconnected environments
Once you have Harper installed and running, create a new project directory and initialize it with Harper's application template:
git clone https://github.com/HarperDB/application-template library-management
cd library-management
With our project structure ready, we'll now define our GraphQL schema to establish our data model.
Defining Our Data Model
In your schema.graphql
file, add the following:
type Book @table @export {
id: ID @primaryKey
title: String!
author: String!
available: Boolean!
borrowedBy: String
}
type Member @table @export {
id: ID @primaryKey
name: String!
email: String!
membershipDate: String!
status: String!
borrowedBooks: [String]
}
This schema defines two entity types:
- Book - Represents a library book with tracking for availability
- Member - Represents a library patron with their borrowing history
The @table
directive instructs Harper to create database tables for these types, while @export
automatically exposes them through REST and GraphQL endpoints. Notice how declarative this is. We're defining both our database structure and API interfaces in a single file.
Implementing Resource Classes
Now let's create the business logic for our API by implementing resource classes in a resources.js file. These classes will handle the HTTP requests to our endpoints. We'll break down the code into manageable sections and explain each one.
The Books Resource Class
First, let's examine the Books
class constructor:
export class Books extends Resource {
constructor() {
super();
this.table = tables.Book;
}
This constructor extends Harper's Resource
class and associates this resource with the Book
table we defined in our GraphQL schema. Harper automatically creates a reference to this table in the tables
object.
Next, let's look at the method for creating new books:
async post(data) {
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (err) {
return { error: "Invalid JSON" };
}
}
data = {
available: true,
...data
};
// Prevent clients from overriding the primary key
if (data && data.id !== undefined) {
delete data.id;
}
try {
const result = await this.table.post(data);
return result;
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
This post method handles creation of new books. It performs several important functions:
- Data Parsing - Ensures the incoming data is properly formatted as an object
- Default Values - Sets the book's availability to true by default
- ID Protection - Removes any client-provided ID to prevent overriding system-generated IDs
- Database Operation - Calls the table's post method to insert the record
- Error Handling - Catches and formats any errors that might occur
The method automatically maps to HTTP POST requests to the /Book
endpoint. Harper handles this routing for us based on class and method names.
Now, let's examine the method for retrieving books:
async get() {
this.id = this.getId();
try {
if (this.id) {
return await this.table.get(this.id);
} else {
return await this.table.get();
}
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
The get
method handles requests to retrieve books. It's versatile, supporting both:
- Fetching a specific book by ID (when an ID is provided in the URL)
- Retrieving all books (when no ID is specified)
Harper's getId()
method automatically extracts the ID from the request URL. This handles URLs like /Book/123
where 123
is the book ID.
Let's continue with the method for updating books:
async put(data) {
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (err) {
return { error: "Invalid JSON" };
}
}
if (!data.id) {
return { error: "Book ID is required for updates" };
}
try {
await this.table.put(data);
return { message: "Update successful" };
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
The put
method handles updating existing books. It follows a similar pattern to the post
method, but with a key difference:
- It requires an ID in the request data
- It uses the table's
put
method, which updates a record rather than creating a new one
Finally, let's look at the method for deleting books:
async delete() {
this.id = this.getId();
if (!this.id) {
return { error: "Book ID is required for deletion" };
}
try {
await this.table.delete(this.id);
return { message: "Delete successful" };
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
}
The delete
method removes a book from the database. It:
- Extracts the book ID from the URL
- Validates that an ID was provided
- Calls the table's
delete
method to remove the record - Returns a success message or error details
With these four methods, we've created a complete API for managing books in our library.
The Members Resource Class
Now let's examine the Members
class, which follows a similar pattern but with specific fields for member management:
export class Members extends Resource {
constructor() {
super();
this.table = tables.Member;
}
Like the Books
class, this constructor extends Harper's Resource
class and associates with the Member
table.
Next, let's look at the method for creating new members:
async post(data) {
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (err) {
return { error: "Invalid JSON" };
}
}
// Set default values
data = {
membershipDate: new Date().toISOString().split('T')[0], // Today's date
status: "ACTIVE",
borrowedBooks: [],
...data
};
if (data && data.id !== undefined) {
delete data.id;
}
try {
const result = await this.table.post(data);
return result;
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
This method handles creation of new members. Unlike the Books
class, it sets several member-specific default values:
membershipDate
- Sets to today's date in YYYY-MM-DD formatstatus
- Sets to "ACTIVE" by defaultborrowedBooks
- Initializes as an empty array
These defaults ensure that new members start with a consistent state, reflecting their real-world status in the library.
The method for retrieving members follows the same pattern as for books:
async get() {
this.id = this.getId();
try {
if (this.id) {
return await this.table.get(this.id);
} else {
return await this.table.get();
}
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
Similar to the Books
class, this method supports both retrieving a specific member by ID and listing all members.
Next, let's examine the method for updating members:
async put(data) {
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (err) {
return { error: "Invalid JSON" };
}
}
if (!data.id) {
return { error: "Member ID is required for updates" };
}
try {
await this.table.put(data);
return { message: "Member updated successfully" };
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
This method validates the input, ensures an ID is provided, and uses the table's put method to update the member record.
Finally, let's look at the method for deleting members:
async delete() {
this.id = this.getId();
if (!this.id) {
return { error: "Member ID is required for deletion" };
}
try {
await this.table.delete(this.id);
return { message: "Delete successful" };
} catch (error) {
return { error: "Internal Server Error", details: error.message };
}
}
}
As with the Books class, this method removes a member from the database after extracting and validating the ID from the URL.
Testing Our Library Management System
Now that we've built our system, let's run it and test each component. Start your Harper instance:
harperdb dev .
Harper automatically:
- Creates the database tables based on our GraphQL schema
- Sets up the REST and GraphQL endpoints
- Connects our resource classes to these endpoints
Now you can make the HTTP calls to your service on http://localhost:9926/
Conclusion
In this article, we've built a Library Management System using Harper. We've implemented CRUD operations for both books and members, creating a foundation that can be extended with additional functionality as needed.
The power of Harper lies in its simplicity and efficiency. By defining our schema in GraphQL and implementing resource classes, we've created a full-featured API without the complexities of connecting separate database and API layers. The system we've built is:
- Scalable - Can handle growing collections of books and members
- Extensible - Built on patterns that can be expanded for new features
- Maintainable - Organized in a clear, modular structure
- Performant - Benefits from Harper's unified runtime
Whether you're building a small personal library tracker or a system for a public institution, Harper provides the tools you need to create robust, efficient APIs without unnecessary complexity.
You can extend this project and add more features, it’d be nice to see what you build.