May 13, 2025

Building a Library Management System with Harper

Welcome to Community Posts
Click below to read the full article.
Arrow
Summary of What to Expect
Table of Contents

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:

  1. Extracts the book ID from the URL
  2. Validates that an ID was provided
  3. Calls the table's delete method to remove the record
  4. 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 format
  • status - Sets to "ACTIVE" by default
  • borrowedBooks - 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:

  1. Creates the database tables based on our GraphQL schema
  2. Sets up the REST and GraphQL endpoints
  3. 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.