Building a CRUD API with Total.js: A Step-by-Step Guide

Louis BertsonLouis Bertson
6 min read

Total.js 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.

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 by running:

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:

// /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:

// /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:

// /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:

node index.js

You can then use tools like Postman 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:

{
    "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.

  1. 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.

{
"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.

  1. 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

This documentation provides in-depth insights into:

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. 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!

0
Subscribe to my newsletter

Read articles from Louis Bertson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Louis Bertson
Louis Bertson

I am a happy developer. I choose quality over quantity because it makes me happier