Building a CRUD API with Total.js: A Step-by-Step Guide
Tags: api, totaljs, rest, restapi
Published: 2023-11-15 14:53:06

[Total.js](https://www.totaljs.com/) is a powerful web application framework for Node.js, providing an excellent foundation for building robust APIs. In this tutorial, we'll guide you through the process of creating a **CRUD** (Create, Read, Update, Delete) API for managing a collection of books using [Total.js](https://www.totaljs.com/).

## Table of Contents

1. **Prerequisites**
2. **Setting Up the Project**
3. **Defining API Routes**
4. **Creating the 'Books' Schema**
5. **Testing the API**
6. **Testing API Routing for WebSockets**
7. **Additional Resources**


## 1. Prerequisites

Before we start, make sure you have **Node.js** and **npm** installed on your machine. You can install [Total.js](https://www.totaljs.com/) by running:

```bash
npm install total4
```

## 2. Setting Up the Project

Create a new file named `index.js` in your project folder and add the following code to enable debugging:

```javascript
// /index.js
require('total4/debug')({ port: 4000 });
```

## 3. Defining **API Routes**

Now, let's define the routes for our **CRUD** operations. In the `controllers/api.js` file, set up the standard **RESTful** routing and additional conventions for **API** routing:

```js
// /controllers/api.js
exports.install = function() {
    // Standard **RESTful** routing
    ROUTE('GET      /api/books/         *Books --> query');   // Query all books
    ROUTE('GET      /api/books/{id}/    *Books --> read');    // Read a specific book by ID
    ROUTE('POST     /api/books/         *Books --> insert');  // Insert a new book (+ expects payload data, validated in schema)
    ROUTE('PUT      /api/books/{id}/    *Books --> update');  // Update a specific book by ID (+ expects payload data, validated in schema)
    ROUTE('DELETE   /api/books/{id}/    *Books --> remove');  // Remove a specific book by ID (- does not expect payload data)

    // **API** routing using different conventions

    // Using hyphen notation for **API** endpoint names
    ROUTE('API /api/ -books             *Books --> query');   // **API**: Query all books (- does not expect payload data)
    ROUTE('API /api/ -books_read/{id}   *Books --> read');    // **API**: Read a specific book by ID (- does not expect payload data)
    ROUTE('API /api/ +books_insert      *Books --> insert');  // **API**: Insert a new book (+ expects payload data, validated in schema)
    ROUTE('API /api/ +books_update/{id} *Books --> update');  // **API**: Update a specific book by ID (+ expects payload data, validated in schema)
    ROUTE('API /api/ -books_remove/{id} *Books --> remove');  // **API**: Remove a specific book by ID (- does not expect payload data)

    // **API** routing for websockets using at notation
    ROUTE('API @api -books              *Books --> query');   // **API Websocket**: Query all books (- does not expect payload data)
    ROUTE('API @api -books_read/{id}    *Books --> read');    // **API Websocket**: Read a specific book by ID (- does not expect payload data)
    ROUTE('API @api +books_insert       *Books --> insert');  // **API Websocket**: Insert a new book (+ expects payload data, validated in schema)
    ROUTE('API @api +books_update/{id}  *Books --> update');  // **API Websocket**: Update a specific book by ID (+ expects payload data, validated in schema)
    ROUTE('API @api -books_remove/{id}  *Books --> remove');  // **API Websocket**: Remove a specific book by ID (- does not expect payload data)

    // For WEBSOCKET Routing
    ROUTE('SOCKET /api/ @api', 1024 * 5);                                 // Handle websockets on the '/api/' path using at notation
}
```

## 4. Creating the 'Books' Schema

Define the schema for the 'Books' entity in the `schemas/books.js` file. This schema will include actions for querying, reading, inserting, updating, and removing books:

```javascript
// /schemas/books.js
// Create an empty array to store book data
var Books = [];

// Define a new schema for 'Books'
NEWSCHEMA('Books', function(schema) {

    // Action for querying books
    schema.action('query', {
        name: 'Listing books',
        query: 'search:String',
        action: function($) {
            // Callback with the array of books
            $.callback(Books);
        }
    });

    // Action for reading a specific book
    schema.action('read', {
        name: 'Read a specific book',
        params: '*id:UID',
        action: function($) {
            var params = $.params;
            // Find the book by ID in the array
            var item = Books.findItem('id', params.id);
            if (!item) {
                // If the book is not found, return a 404 error
                $.invalid(404);
                return;
            }
            // Callback with the found book
            $.callback(item);
        }
    });

    // Action for inserting a new book
    schema.action('insert', {
        name: 'Insert new

 book',
        input: '*title:String,description:String,price:Number,author:String,year:String',
        action: function($, model) {
            // Generate a unique ID and set the creation timestamp
            model.id = UID();
            model.dtcreated = NOW;
            // Create a search string for the book
            model.search = (model.title + ' ' + model.description + ' ' + model.author + ' ' + model.year).toSearch();
            // Add the new book to the array
            Books.push(model);
            // Callback with the ID of the inserted book
            $.done(model.id)();
        }
    });

    // Action for updating a book
    schema.action('update', {
        name: 'Update book',
        params: '*id:String',
        input: '*title:String,description:String,price:Number,author:String,year:String',
        action: function($, model) {
            var item = Books.findItem('id', $.params.id);
            if (!item) {
                // If the book is not found, return a 404 error
                $.invalid(404);
                return;
            }
            // Update the book with the new data
            model.dtupdated = NOW;
            model.search = (model.title + ' ' + model.description + ' ' + model.author + ' ' + model.year).toSearch();
            for (var el in model) 
                item[el] = model[el];
            // Callback with the ID of the updated book
            $.done(model.id)();
        }
    });

    // Action for removing a book
    schema.action('remove', {
        name: 'Remove book',
        params: '*id:String',
        action: function($) {
            var params = $.params;
            // Find the index of the book in the array
            var index = Books.findIndex('id', params.id);
            console.log(index);
            if (index === -1) {
                // If the book is not found, return a 404 error
                $.invalid(404);
                return;
            }
            // Remove the book from the array
            Books.remove(item => item.id == index);
            // Callback to indicate successful removal
            $.done()();
        } 
    });
});
```

## 5. Testing the **API**

Now that our **API** is set up, it's time to test it. Start your application by running:

```bash
node index.js
```

You can then use tools like [Postman](https://www.postman.com/) or cURL to interact with your **API** and perform **CRUD** operations on the 'Books' entity.

## 6. Testing API Routing for WebSockets

With Total.js, testing WebSocket functionality is made easy. The framework allows you to test API routing for WebSockets using messages. Here's an example WebSocket message that you can use for testing:

```json
{
    "TYPE": "api",
    "callbackid": "CUSTOM_CALLBACK_ID", // The value will be returned back
    "data": {
        "schema": "schema_name/{dynamic_arg_1}/{dynamic_arg_2}?query=arguments",
        "data": {}
    }
}
```

### Message Components:

- **TYPE:** Indicates that this is an API-related message.
- **callbackid:** A custom callback ID that will be returned back in the response.
- **data:** Contains information about the API request.
  - **schema:** Specifies the schema name and dynamic arguments along with query parameters.
  - **data:** An object containing additional data for the API request.

### Example Usage:

1. **Connecting to WebSocket:**

   Use a WebSocket client or tool to connect to the WebSocket endpoint of your Total.js application.

2. **Sending the WebSocket Message:**

   Send the provided WebSocket message to the server. Make sure to replace placeholders such as `CUSTOM_CALLBACK_ID`, `schema_name`, `{dynamic_arg_1}`, `{dynamic_arg_2}`, and `arguments` with actual values.

```json
{
"TYPE": "api",
"callbackid": "CUSTOM_CALLBACK_ID",
"data": {
"schema": "books_read/123456789/query?search=Total.js",
"data": {}
}
}
```

   This example message is querying the `books_read` schema for a book with the ID `123456789` and includes a search query parameter.

3. **Receiving the Response:**

   The server will process the WebSocket message, execute the corresponding API route, and send a response back. Look for the response, including the custom callback ID, to verify the success of the API routing.

By following this approach, you can effectively test and debug your WebSocket-based API routes in Total.js.

## Additional Resources

For a comprehensive understanding of Total.js API routing mechanisms and features, refer to the official [Total.js documentation](https://docs.totaljs.com/total4/)

This documentation provides in-depth insights into:

- [Standard RESTful routing](https://docs.totaljs.com/total4/40d57002oi50c/)
- [API routing conventions](https://docs.totaljs.com/total4/cc674001rr51c)
- [WebSocket routing using various notations](https://docs.totaljs.com/total4/40d57002oi50c/#websocket-routing)
- [Handling WebSocket messages for API Routing](https://docs.totaljs.com/total4/cc674001rr51c#websocket-api)

Explore the documentation to enhance your knowledge and leverage the full potential of Total.js for building powerful and scalable APIs.

## Conclusion

Congratulations! You've successfully created a **CRUD API** using [Total.js](https://www.totaljs.com/). This simple yet powerful framework allows you to build scalable and maintainable **APIs** with ease.

Feel free to customize and extend this project according to your specific requirements. Happy coding!