From bare-bone AJAX to REST API

As web applications evolve, transitioning from bare-bone AJAX (Asynchronous JavaScript and XML) calls to a more robust and standardized REST (Representational State Transfer) API becomes essential. I take the journey for the first time with LWT 2.9.0, a version of LWT released December 2024.

REST APIs enhance scalability, maintainability, and interoperability within web projects. In this blog, we will explore the step-by-step process of making this transition, unlocking the potential for improved development practices and efficient communication between the client and server.

Understanding the Basics of REST API

To begin the transition, it is crucial to grasp the core principles of a REST API. REST operates on the HTTP protocol, utilizing methods such as GET, POST, PUT, and DELETE to interact with resources. I familiarized myself with the fundamental concepts of resource identification via URIs (Uniform Resource Identifiers) and stateless communication.

Identify and Expose Resources

All AJAX files in LWT
Server-side definition of AJAX endpoints: a bunch of files


The difficulty with LWT is to move from one system to another without breaking anything. Until 2.9.0, when a server resource was needed, a new AJAX file was created, without uniformity with the others. I had a great time identifying the data and functionalities that needed to be exposed via the API. You should start by creating a resource inventory, including endpoints for each resource/action you want to map. This process ensures all relevant functionalities are adequately covered and aligns with REST principles.

URIs Design and Definition

## GPT 3

Sure! Based on the provided resources and actions, here is a suggestion for a coherent REST API design:

1. GET API Endpoints:
   - Get API Version: `GET /api/version`
   - Get Next Word to Test: `GET /api/test/next-word`
   - Get Tomorrow's Tests Number: `GET /api/test/tomorrow`
   - Get Phonetic Reading: `GET /api/text/phonetic-reading`
   - Get Theming Path: `GET /api/text/theme-path`
   - Get Texts Statistics: `GET /api/text/statistics`
   - Get Media Paths: `GET /api/media/paths`
   - Get Example Sentences: `GET /api/sentences/{word}`
   - Get Imported Terms: `GET /api/terms/imported`
   - Get Similar Terms: `GET /api/terms/{term}/similar`
   - Get Term Translations: `GET /api/terms/{term}/translations`

2. POST API Endpoints:
   - Update Reading Position: `POST /api/reading/position`
   - Add/Update Translation: `POST /api/translation/{word}`
   - Increment/Decrement Term Status: `POST /api/terms/{term}/status`
   - Set Term Status: `POST /api/terms/{term}/status/set`
   - Test Regular Expression: `POST /api/regexp/test`
   - Set Term Annotation: `POST /api/terms/{term}/annotation`
   - Save Setting: `POST /api/settings`


Once your resources are identified, design and define the URIs for each of them. Follow REST conventions, as they ensure consistency and meaningful representations. Employ names that clearly describe the resource and avoid including irrelevant details.

Proper HTTP Methods

Some existing content was asked for on POST, and some was posted on GET… I aligned the appropriate HTTP methods (GET, POST, PUT, DELETE) to perform actions on your resources. Map these actions to the corresponding endpoints based on your application’s requirements. This aligns your AJAX calls with the REST architectural style, simplifying the architecture and promoting scalability.

RESTful Response Formats

The previous APIs used random format, for the record of oddities:

  • Answer as an HTML string to display a page
  • JavaScript to be ran through eval
  • SQL to be ran by the server

Update your AJAX calls to expect and handle responses in a RESTful format. Using JSON (JavaScript Object Notation) as the standard data format enhances interoperability and simplifies integration with other applications. Ensure your backend server responds with the correct HTTP status codes to denote the success or failure of a request.

API Versioning

As your REST API evolves, it is essential to introduce versioning early on to maintain backward compatibility. Utilize a versioning strategy (e.g., through URL parameters or headers) to manage and support different API versions. This fosters a clear separation between old and new endpoints and allows clients to migrate gracefully.

Update and Replace AJAX Calls

As the backend REST API implementation progresses, gradually replace the existing bare-bone AJAX calls with calls to the newly developed REST endpoints. Refactor your frontend JavaScript code to utilize these RESTful endpoints, adhering to the REST conventions.

Test and Iterate

As it was a big task, I had to test everything but could not do it manually. So I added an NPM package to LWT, and a test framework using Chai and supertest. Ensure the REST API is functioning as intended, and all previous AJAX functionality has been correctly refactored. Gather feedback from users and developers, iterating and optimizing the API based on observed performance or usability issues.

As the API is growing in maturity, you should see new repeated patterns. Once you have a better understanding of how things go, it is a good idea to iterate. I my initial post, I defined an initial set of endpoints, then I realized that some arguments were repetitive or irrelevant, which let me to new structure better tailored to my needs.

Final result

Now that we defined the endpoints, standardized the format implemented a solution and tested it, let’s have a look at our new API. The full code is in lwt/api.php, if you want to have a look.

function request_handler($method, $requestUri, $post_param) {
    // Extract requested endpoint from URI
    $req_endpoint = endpoint_exits($method, $requestUri);
    $endpoint_fragments = preg_split("/\//", $req_endpoint);

    // Process endpoint request
    if ($method === 'GET') {
        // Handle GET request for each endpoint
        $uri_query = parse_url($requestUri, PHP_URL_QUERY);
        if ($uri_query == null) {
            $req_param = array();
        } else {
            parse_str($uri_query, $req_param);
        }
        
        switch ($endpoint_fragments[0]) {
            case 'media-files':
                $answer = media_files($req_param);
                send_response(200, $answer);
                break;
            case 'phonetic-reading':
                $answer = get_phonetic_reading($req_param);
                send_response(200, $answer);
                break; 
            case 'review':
                switch ($endpoint_fragments[1]) {
                    case 'next-word':
                        $answer = word_test_ajax($req_param);
                        send_response(200, $answer);
                        break;
                    case 'tomorrow-count':
                        $answer = tomorrow_test_count($req_param);
                        send_response(200, $answer);
                        break;
                    default:
                        send_response(
                            404, 
                            ['error' => 'Endpoint Not Found' . 
                            $endpoint_fragments[1]]
                        );
                }
                break;
            case 'sentences-with-term':
                // Process
                break;
            case 'similar-terms':
                // Process
                break;
            case 'settings':
                switch ($endpoint_fragments[1]) {
                    case 'theme-path':
                        $answer = get_theme_path($req_param);
                        send_response(200, $answer);
                        break;
                    default:
                        send_response(
                            404, 
                            ['error' => 'Endpoint Not Found: ' . 
                            $endpoint_fragments[1]]
                        );
                }
                break;
            case 'terms':
                // Process
                break;
            case 'texts':
                // Process
            case 'version':
                $answer = rest_api_version($req_param);
                send_response(200, $answer);
                break;
            // Add more GET handlers for other endpoints
            default:
                send_response(
                    404, 
                    ['error' => 'Endpoint Not Found: ' . 
                    $endpoint_fragments[0]]
                );
        }
    } elseif ($method === 'POST') {
        // Handle POST request for each endpoint
        switch ($endpoint_fragments[0]) {
            case 'settings':
                $answer = save_setting($post_param);
                send_response(200, $answer);
                break;
            case 'texts':
                if (!ctype_digit($endpoint_fragments[1])) {
                    send_response(
                        404, 
                        ['error' => 'Text ID (Integer) Expected, Got ' . 
                        $endpoint_fragments[1]]
                    );
                }
                $post_param["text_id"] = (int) $endpoint_fragments[1];
                switch ($endpoint_fragments[2]) {
                    case 'annotation':
                        $answer = set_annotation($post_param);
                        send_response(200, $answer);
                        break;
                    case 'audio-position':
                        $answer = set_audio_position($post_param);
                        send_response(200, $answer);
                        break;
                    case 'reading-position':
                        $answer = set_text_position($post_param);
                        send_response(200, $answer);
                        break;
                    default:
                        send_response(
                            404, 
                            ['error' => 'Endpoint Not Found: ' . 
                            $endpoint_fragments[2]]
                        );
                }
                break;
            case 'terms':
                // Process
                break;
            default:
                send_response(
                    404, 
                    ['error' => 'Endpoint Not Found On POST: ' . 
                    $endpoint_fragments[0]]
                );
        }
    }
}

Now, let’s look a client would request a new word (where doing a word remembering test), in JS. You can find the full code at lwt/do_test_test.php#483.


/**
 * Get a new word test.
 */
function get_new_word()
{
    // Get new word through AJAX
    const review_data = <?php echo json_encode(array(
      "total_tests" => $count,
      "test_key" => $selector,
      "selection" => $selection,
      "word_mode" => $nosent,
      "lg_id" => $lgid,
      "word_regex" => (string)$lang['regexword'],
      "type" => $testtype
    )); ?>;
            
    $.getJSON(
        'inc/ajax.php/v1/tests/next-word?' + $.param(review_data)
    ).done(test_query_handler);

    // Close any previous tooltip
    cClick();
}

Now, even complex requests now use a much more simple format. This specific feature was previously sending SQL code through AJAX, now we only send the minimal information.

Conclusion

Transitioning from bare-bone AJAX calls to a REST API initially requires significant effort, but it results in a more scalable, maintainable, and interoperable web application. Embracing REST API not only improves the development process but also enables seamless integration with other systems and fosters a more standardized approach to web application design.