zoraxy/docs/plugins/docs/3. Basic Examples/2. RESTful Example.md
Toby Chui ddb1a8773e Added restful-api plugin
- Added restful-api example plugin
- Added more plugin docs
- Added zoraxy_plugin HandleFunc API
2025-05-30 15:57:59 +08:00

19 KiB

RESTful API Call in Web UI

Last Update: 29/05/2025


When developing a UI for your plugin, sometime you might need to make RESTFUL API calls to your plugin backend for setting up something or getting latest information from your plugin. In this example, I will show you how to create a plugin with RESTful api call capabilities with the embedded web server and the custom cjax function.

Notes: This example assumes you have basic understanding on how to use jQuery ajax request.

Lets get started!


1. Create the plugin folder structures

This step is identical to the Hello World example, where you create a plugin folder with the required go project structure in the folder. Please refer to the Hello World example section 1 to 5 for details.


2. Create Introspect

This is quite similar to the Hello World example as well, but we are changing some of the IDs to match what we want to do in this plugin.

runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
		ID:            "com.example.restful-example",
		Name:          "Restful Example",
		Author:        "foobar",
		AuthorContact: "admin@example.com",
		Description:   "A simple demo for making RESTful API calls in plugin",
		URL:           "https://example.com",
		Type:          plugin.PluginType_Utilities,
		VersionMajor:  1,
		VersionMinor:  0,
		VersionPatch:  0,

		// As this is a utility plugin, we don't need to capture any traffic
		// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
		UIPath: UI_PATH,
	})
	if err != nil {
		//Terminate or enter standalone mode here
		panic(err)
	}

3. Create an embedded web server with handlers

In this step, we create a basic embedded web file handler similar to the Hello World example, however, we will need to add a http.HandleFunc to the plugin so our front-end can request and communicate with the backend.

embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
embedWebRouter.RegisterTerminateHandler(func() {
    fmt.Println("Restful-example Exited")
}, nil)

//Register a simple API endpoint that will echo the request body
// Since we are using the default http.ServeMux, we can register the handler directly with the last
// parameter as nil
embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
    //Some handler code here
}, nil)

The embedWebRouter.HandleFunc last parameter is the http.Mux, where if you have multiple web server listening interface, you can fill in different Mux based on your implementation. On of the examples is that, when you are developing a static web server plugin, where you need a dedicated HTTP listening endpoint that is not the one Zoraxy assigned to your plugin, you need to create two http.Mux and assign one of them for Zoraxy plugin UI purpose.


4. Modify the front-end HTML file to make request to backend

To make a RESTFUL API to your plugin, you must use relative path in your request URL.

Absolute path that start with / is only use for accessing Zoraxy resouces. For example, when you access /img/logo.svg, Zoraxy webmin HTTP router will return the logo of Zoraxy for you instead of /plugins/your_plugin_name/{your_web_root}/img/logo.svg.

Making GET request

Making GET request is similar to what you would do in ordinary web development, but only limited to relative paths like ./api/foo/bar instead. Here is an example on a front-end and back-end implementation of a simple "echo" API.

The API logic is simple: when you make a GET request to the API with ?name=foobar, it returns Hello foobar. Here is the backend implementation in your plugin code.

embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
		// This is a simple echo API that will return the request body as response
		name := r.URL.Query().Get("name")
		if name == "" {
			http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
		if err := json.NewEncoder(w).Encode(response); err != nil {
			http.Error(w, "Failed to encode response", http.StatusInternalServerError)
		}
	}, nil)

And here is the front-end code in your HTML file

<!-- The example below show how HTTP GET is used with Zoraxy plugin -->
<h3>Echo Test (HTTP GET)</h3>
<div class="ui form">
    <div class="field">
        <label for="nameInput">Enter your name:</label>
        <input type="text" id="nameInput" placeholder="Your name">
    </div>
    <button class="ui button primary" id="sendRequestButton">Send Request</button>
    <div class="ui message" id="responseMessage" style="display: none;"></div>
</div>

<script>
    document.getElementById('sendRequestButton').addEventListener('click', function() {
        const name = document.getElementById('nameInput').value;
        if (name.trim() === "") {
            alert("Please enter a name.");
            return;
        }
        // Note the relative path is used here!
        // GET do not require CSRF token, so you can use $.ajax directly
        // or $.cjax (defined in /script/utils.js) to make GET request
        $.ajax({
            url: `./api/echo`,
            type: 'GET',
            data: { name: name },
            success: function(data) {
                console.log('Response:', data.message);
                $('#responseMessage').text(data.message).show();
            },
            error: function(xhr, status, error) {
                console.error('Error:', error);
                $('#responseMessage').text('An error occurred while processing your request.').show();
            }
        });
    });
</script>

Making POST request

Making POST request is also similar to GET request, except when making the request, you will need pass the CSRF-Token with the payload. This is required due to security reasons (See #267 for more details).

Since the CSRF validation is done by Zoraxy, your plugin backend code can be implemented just like an ordinary handler. Here is an example POST handling function that receive a FORM POST and print it in an HTML response.

embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
        return
    }

    if err := r.ParseForm(); err != nil {
        http.Error(w, "Failed to parse form data", http.StatusBadRequest)
        return
    }

    for key, values := range r.PostForm {
        for _, value := range values {
            // Generate a simple HTML response
            w.Header().Set("Content-Type", "text/html")
            fmt.Fprintf(w, "%s: %s<br>", key, value)
        }
    }
}, nil)
	

For the front-end, you will need to use the $.cjax function implemented in Zoraxy utils.js file. You can include this file by adding these two lines to your HTML file.

<script src="/script/jquery-3.6.0.min.js"></script>
<script src="/script/utils.js"></script>
<!- More lines here -->
<!-- The example below shows how form post can be used in plugin -->
<h3>Form Post Test (HTTP POST)</h3>
<div class="ui form">
    <div class="field">
        <label for="postNameInput">Name:</label>
        <input type="text" id="postNameInput" placeholder="Your name">
    </div>
    <div class="field">
        <label for="postAgeInput">Age:</label>
        <input type="number" id="postAgeInput" placeholder="Your age">
    </div>
    <div class="field">
        <label>Gender:</label>
        <div class="ui checkbox">
            <input type="checkbox" id="genderMale" name="gender" value="Male">
            <label for="genderMale">Male</label>
        </div>
        <div class="ui checkbox">
            <input type="checkbox" id="genderFemale" name="gender" value="Female">
            <label for="genderFemale">Female</label>
        </div>
    </div>
    <button class="ui button primary" id="postRequestButton">Send</button>
    <div class="ui message" id="postResponseMessage" style="display: none;"></div>
</div>

After that, you can call to the $.cjax function just like what you would usually do with the $ajax function.

// .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
// alternatively you can use $.ajax with CSRF token in headers
// the header is named "X-CSRF-Token" and the value is taken from the head
// meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
$.cjax({
    url: './api/post',
    type: 'POST',
    data: { name: name, age: age, gender: gender },
    success: function(data) {
    console.log('Response:', data);
    	$('#postResponseMessage').html(data).show();
    },
    error: function(xhr, status, error) {
        console.error('Error:', error);
        $('#postResponseMessage').text('An error occurred while processing your request.').show();
    }
});

POST Request with Vanilla JS

It is possible to make POST request with Vanilla JS. Note that you will need to populate the csrf-token field yourself to make the request pass through the plugin UI request router in Zoraxy. Here is a basic example on how it could be done.

fetch('./api/post', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken // Include the CSRF token in the headers
    },
    body: JSON.stringify({{your_data_here}})
})

5. Full Code

Here is the full code of the RESTFUL example for reference.

Front-end (plugins/restful-example/www/index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!-- CSRF token, if your plugin need to make POST request to backend -->
    <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
    <link rel="stylesheet" href="/script/semantic/semantic.min.css">
    <script src="/script/jquery-3.6.0.min.js"></script>
    <script src="/script/semantic/semantic.min.js"></script>
    <script src="/script/utils.js"></script>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/main.css">
    <title>RESTful Example</title>
    <style>
        body {
            background-color: var(--theme_bg_primary);
        }
    </style>
</head>
<body>
    <!-- Dark theme script must be included after body tag-->
    <link rel="stylesheet" href="/darktheme.css">
    <script src="/script/darktheme.js"></script>
    <br>
    <div class="standardContainer">
        <div class="ui container">
            <h1>RESTFul API Example</h1>
            <!-- The example below show how HTTP GET is used with Zoraxy plugin -->
            <h3>Echo Test (HTTP GET)</h3>
            <div class="ui form">
                <div class="field">
                    <label for="nameInput">Enter your name:</label>
                    <input type="text" id="nameInput" placeholder="Your name">
                </div>
                <button class="ui button primary" id="sendRequestButton">Send Request</button>
                <div class="ui message" id="responseMessage" style="display: none;"></div>
            </div>

            <script>
                document.getElementById('sendRequestButton').addEventListener('click', function() {
                    const name = document.getElementById('nameInput').value;
                    if (name.trim() === "") {
                        alert("Please enter a name.");
                        return;
                    }
                    // Note the relative path is used here!
                    // GET do not require CSRF token, so you can use $.ajax directly
                    // or $.cjax (defined in /script/utils.js) to make GET request
                    $.ajax({
                        url: `./api/echo`,
                        type: 'GET',
                        data: { name: name },
                        success: function(data) {
                            console.log('Response:', data.message);
                            $('#responseMessage').text(data.message).show();
                        },
                        error: function(xhr, status, error) {
                            console.error('Error:', error);
                            $('#responseMessage').text('An error occurred while processing your request.').show();
                        }
                    });
                });
            </script>
            <!-- The example below shows how form post can be used in plugin -->
            <h3>Form Post Test (HTTP POST)</h3>
            <div class="ui form">
                <div class="field">
                    <label for="postNameInput">Name:</label>
                    <input type="text" id="postNameInput" placeholder="Your name">
                </div>
                <div class="field">
                    <label for="postAgeInput">Age:</label>
                    <input type="number" id="postAgeInput" placeholder="Your age">
                </div>
                <div class="field">
                    <label>Gender:</label>
                    <div class="ui checkbox">
                        <input type="checkbox" id="genderMale" name="gender" value="Male">
                        <label for="genderMale">Male</label>
                    </div>
                    <div class="ui checkbox">
                        <input type="checkbox" id="genderFemale" name="gender" value="Female">
                        <label for="genderFemale">Female</label>
                    </div>
                </div>
                <button class="ui button primary" id="postRequestButton">Send</button>
                <div class="ui message" id="postResponseMessage" style="display: none;"></div>
            </div>

            <script>
                document.getElementById('postRequestButton').addEventListener('click', function() {
                    const name = document.getElementById('postNameInput').value;
                    const age = document.getElementById('postAgeInput').value;
                    const genderMale = document.getElementById('genderMale').checked;
                    const genderFemale = document.getElementById('genderFemale').checked;

                    if (name.trim() === "" || age.trim() === "" || (!genderMale && !genderFemale)) {
                        alert("Please fill out all fields.");
                        return;
                    }

                    const gender = genderMale ? "Male" : "Female";
                    
                    // .cjax (defined in /script/utils.js) is used to make POST request with CSRF token support
                    // alternatively you can use $.ajax with CSRF token in headers
                    // the header is named "X-CSRF-Token" and the value is taken from the head
                    // meta tag content (i.e. <meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">)
                    $.cjax({
                        url: './api/post',
                        type: 'POST',
                        data: { name: name, age: age, gender: gender },
                        success: function(data) {
                            console.log('Response:', data);
                            $('#postResponseMessage').html(data).show();
                        },
                        error: function(xhr, status, error) {
                            console.error('Error:', error);
                            $('#postResponseMessage').text('An error occurred while processing your request.').show();
                        }
                    });
                });
            </script>
        </div>
    </div>
</body>
</html>

Backend (plugins/restful-example/main.go)

package main

import (
	"embed"
	_ "embed"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"

	plugin "example.com/zoraxy/restful-example/mod/zoraxy_plugin"
)

const (
	PLUGIN_ID = "com.example.restful-example"
	UI_PATH   = "/"
	WEB_ROOT  = "/www"
)

//go:embed www/*
var content embed.FS

func main() {
	// Serve the plugin intro spect
	// This will print the plugin intro spect and exit if the -introspect flag is provided
	runtimeCfg, err := plugin.ServeAndRecvSpec(&plugin.IntroSpect{
		ID:            "com.example.restful-example",
		Name:          "Restful Example",
		Author:        "foobar",
		AuthorContact: "admin@example.com",
		Description:   "A simple demo for making RESTful API calls in plugin",
		URL:           "https://example.com",
		Type:          plugin.PluginType_Utilities,
		VersionMajor:  1,
		VersionMinor:  0,
		VersionPatch:  0,

		// As this is a utility plugin, we don't need to capture any traffic
		// but only serve the UI, so we set the UI (relative to the plugin path) to "/"
		UIPath: UI_PATH,
	})
	if err != nil {
		//Terminate or enter standalone mode here
		panic(err)
	}

	// Create a new PluginEmbedUIRouter that will serve the UI from web folder
	// The router will also help to handle the termination of the plugin when
	// a user wants to stop the plugin via Zoraxy Web UI
	embedWebRouter := plugin.NewPluginEmbedUIRouter(PLUGIN_ID, &content, WEB_ROOT, UI_PATH)
	embedWebRouter.RegisterTerminateHandler(func() {
		// Do cleanup here if needed
		fmt.Println("Restful-example Exited")
	}, nil)

	//Register a simple API endpoint that will echo the request body
	// Since we are using the default http.ServeMux, we can register the handler directly with the last
	// parameter as nil
	embedWebRouter.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
		// This is a simple echo API that will return the request body as response
		name := r.URL.Query().Get("name")
		if name == "" {
			http.Error(w, "Missing 'name' query parameter", http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		response := map[string]string{"message": fmt.Sprintf("Hello %s", name)}
		if err := json.NewEncoder(w).Encode(response); err != nil {
			http.Error(w, "Failed to encode response", http.StatusInternalServerError)
		}
	}, nil)

	// Here is another example of a POST API endpoint that will echo the form data
	// This will handle POST requests to /api/post and return the form data as response
	embedWebRouter.HandleFunc("/api/post", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
			return
		}

		if err := r.ParseForm(); err != nil {
			http.Error(w, "Failed to parse form data", http.StatusBadRequest)
			return
		}

		for key, values := range r.PostForm {
			for _, value := range values {
				// Generate a simple HTML response
				w.Header().Set("Content-Type", "text/html")
				fmt.Fprintf(w, "%s: %s<br>", key, value)
			}
		}
	}, nil)

	// Serve the restful-example page in the www folder
	http.Handle(UI_PATH, embedWebRouter.Handler())
	fmt.Println("Restful-example started at http://127.0.0.1:" + strconv.Itoa(runtimeCfg.Port))
	err = http.ListenAndServe("127.0.0.1:"+strconv.Itoa(runtimeCfg.Port), nil)
	if err != nil {
		panic(err)
	}

}

What you should expect to see if everything is correctly loaded and working in Zoray

![image-20250530153148506](img/2. RESTful Example/image-20250530153148506.png)