Blog

Labels
AJAX(112) Apple(1) Application Builder(242) Application Factory(207) ASP.NET(95) ASP.NET 3.5(45) ASP.NET Code Generator(72) ASP.NET Membership(28) Azure(18) Barcodes(3) BLOB(18) Business Rules(1) Business Rules/Logic(140) BYOD(13) Caching(2) Calendar(5) Charts(29) Cloud(14) Cloud On Time(2) Cloud On Time for Windows 7(2) Code Generator(54) Collaboration(11) command line(1) Conflict Detection(1) Content Management System(11) COT Tools for Excel(26) CRUD(1) Custom Actions(1) Data Aquarium Framework(122) Data Sheet(9) Data Sources(22) Database Lookups(50) Deployment(22) Designer(177) DotNetNuke(12) EASE(20) Email(6) Features(99) Firebird(1) Form Builder(14) Globalization and Localization(6) Hypermedia(2) Installation(4) JavaScript(20) Kiosk(1) Low Code(3) Mac(1) Many-To-Many(4) Maps(6) Master/Detail(36) Microservices(4) Mobile(63) Mode Builder(3) Model Builder(3) MySQL(10) Native Apps(5) News(15) OAuth(5) OAuth Scopes(1) OAuth2(6) Offline(14) Oracle(10) PKCE(1) PostgreSQL(2) QR codes(2) Rapid Application Development(5) Reading Pane(2) Release Notes(163) Reports(48) REST(26) RESTful(21) RESTful Workshop(13) RFID tags(1) SaaS(7) Security(75) SharePoint(12) SPA(5) SQL Anywhere(3) SQL Server(26) Stored Procedure(4) Teamwork(15) Tips and Tricks(81) Tools for Excel(2) Touch UI(93) Transactions(5) Tutorials(183) Universal Windows Platform(3) User Interface(331) Video Tutorial(37) Web 2.0(100) Web App Generator(101) Web Application Generator(607) Web Form Builder(39) Web.Config(9) Workflow(28)
Archive
Blog
Monday, May 2, 2022PrintSubscribe
ETag and HTTP Caching

RESTful API Engine fully embraced the HTTP features that were invented to make the Internet faster.

ETag

A web resource may have an optional ETag header also known as an entity tag. Its value must be a unique identifier for the resource content. The engine sets the ETag header to the hash value of the current version of the resource. Web browsers associate the entity tag with the URL of the fetched resource automatically.

If-None-Match

Browsers specify the entity tag in the If-None-Match header when a web application makes the next attempt to get the previously fetched resource. The engine produces the response data, calculates its hash value, and compares the new ETag with the If-None-Match value in the request header. If both values match, then there is no need to transfer the resource content to the client. The engine will set the response status to 304 Not Modified and keep the response body empty. Browsers understand this status code as the instruction to use the previously fetched content.

The ETag and If-None-Match headers provide a simple mechanism to reduce the volume of the client/server traffic. This is especially important when BLOB resources are fetched by the client app repeatedly. Large photos, video, and sound clips are downloaded to the client only once if there are no changes. The subsequent requests to the same resource will result in “304” responses from the server as long as its content has not changed.

The network trace shows two GET requests fetching the list of products. The first request with the "200" status has fetched the resource data. The second request with the "304" status has fetched an empty body and had to re-use the cached data of the previous response instead.
The network trace shows two GET requests fetching the list of products. The first request with the "200" status has fetched the resource data. The second request with the "304" status has fetched an empty body and had to re-use the cached data of the previous response instead.

The performance gains are achieved through the reduced response size. The engine will still produce the resource content on the server and calculate its hash values in response to each network request from the client.

If-Match

CRUD operations may find another use for the entity tags. The entity tag specified in the If-Match header of the request will let the RESTful API Engine to detect the midair collisions. The engine will figure the current ETag of the resource when PUT, POST, PATCH, or DELETE are specified as the HTTP method of the request. If the values of ETag and If-Match are not equal, then the error with the 412 Precondition Failed status is returned. Simply put, the error means that another user has changed the resource after it was fetched by the client app.

The value of the ETag header is automatically copied to the etag property of the _links.self hypermedia control in the resource fetched with the $app.restful method.

Client apps can include the etag property in the method argument when changing data or invoking custom actions. If the original resource content with the modified fields is specified in the body property of the $app.restful method argument, then set the etag to true for conflict detection. Otherwise set the etag value to the _links.self.etag property of the previously fetched resource.

Here is an example of the code that will demonstrate the midair collision.

JavaScript
1234567891011121314151617181920212223242526272829303132333435window.addEventListener('load', e => {
    document.querySelector("h1").addEventListener("click", e => {
        // fetch the arbitrary product resource
        $app.restful({
            url: '/v2/products/17'
        }).then(product => {
            // change the unitPrice
            product.unitPrice = product.unitPrice + 0.1;
            // **** "Edit" Attempt #1 *****
            // Send the modified body to the URL specified in
            // the "edit" hypermedia control to PATCH the resource.
            $app.restful({
                url: product._links.edit,
                body: product,
                etag: true
            }).then(changedProduct => {
                // alert the user to the new UnitPrice
                alert(
                    'The new price of "' + changedProduct.productName +
                    '" is ' + changedProduct.unitPrice);
                // The second attempt to patch the product resource will fail 
                // since the etag in "product._links.self.etag" does not match
                // the ETag of the changed product resource on the server.
                product.unitPrice = product.unitPrice + 0.1;
                // **** "Edit" Attempt #2 *****
                $app.restful({
                    url: product._links.edit,
                    body: product,
                    etag: true
                })
                    .catch(restfulException);
            });
        });
    });
});

Add the code to the bottom of the closure in the spa4.js file of the SPA4 client app. The code will run when users click the h1 element showing the name of the app at the top of the page.

The following sequence of events will take place:

  1. User clicks the header of the app.
  2. The product with the primary key specified in the url parameter of the $app.restful method argument is fetched from the /v2/products/17 resource (see the sample response data).
  3. The unitPrice property of the product variable is increased by $0.01. The variable is pointing to the object fetched in the previous step.
  4. The first attempt to edit the resource is performed. The result of the edit is the new state of the resource in the changedProduct argument. The alert will show the name of the changed product and its unitPrice.
  5. The unitPrice property of the same product variable is increased again by the same amount.
  6. The second attempt to edit the resource is performed with the body property in argument still pointing to the product variable. The $app.restful method will use the “old” entity tag extracted from the variable as the value of the If-Match header. The request fails since the ETag of the resource on the server does not match.

The "GET" request fetches the resource from the server. The first "PATCH" request changes the resource field successfully. The second "PATCH" request fails with the "412 Precondition Failed" status due to the midair conflict.
The "GET" request fetches the resource from the server. The first "PATCH" request changes the resource field successfully. The second "PATCH" request fails with the "412 Precondition Failed" status due to the midair conflict.

The screenshot shows the alert displayed after the first attempt to edit the product.

image9.png

The error message will be reported when the alert is closed since the second attempt to edit the product has failed. The engine will recommend specifying the different entity tag in the If-Match header.

image6.png

Both attempts to edit the product will succeed if the etag property is removed from the $app.restful argument of the second attempt.

ETag is the version number of the resource. Entity tag makes it possible to detect conflicts when specified in the If-Match header.

A better approach is to use the resource that was fetched during the first attempt to edit the product. Replace references to the product variable with the changedProduct and keep etag property for robust conflict detection.

JavaScript
1234567891011121314151617181920212223242526// fetch the arbitrary product resource
$app.restful({
    url: '/v2/products/17'
}).then(product => {
    // change the unitPrice
    product.unitPrice = product.unitPrice + 0.1;
    // **** "Edit" Attempt #1 *****
    $app.restful({
        url: product._links.edit,
        body: product,
        etag: true
    }).then(changedProduct => {
        // alert the user to the new UnitPrice
        alert(
            'The new price of "' + changedProduct.productName +
            '" is ' + changedProduct.unitPrice);
        changedProduct.unitPrice = changedProduct.unitPrice + 0.1;
        // **** "Edit" Attempt #2 *****
        $app.restful({
            "url": changedProduct._links.edit,
            "body": changedProduct,
            "etag": true
        })
            .catch(restfulException);
    });
});

If the body property is not referencing the original resource fetched with the $app.restful method, then set the etag property explicitly. This is the example of the ad-hoc body parameter with the reference to the etag value.

JavaScript
123456789101112131415161718192021222324252627// fetch the arbitrary product resource
$app.restful({
    url: '/v2/products/17'
}).then(product => {
    // change the unitPrice
    product.unitPrice = product.unitPrice + 0.1;
    // **** "Edit" Attempt #1 *****
    $app.restful({
        url: product._links.edit,
        body: product,
        etag: true
    }).then(changedProduct => {
        // alert the user to the new UnitPrice
        alert(
            'The new price of "' + changedProduct.productName +
            '" is ' + changedProduct.unitPrice);
        // **** "Edit" Attempt #2 *****
        $app.restful({
            "url": changedProduct._links.edit,
            "body": {
                unitPrice: changedProduct.unitPrice + 0.1
            },
            "etag": changedProduct._links.self.etag
        })
            .catch(restfulException);
    });
});

Conflict detection requires a certain amount of work on the server. Do not specify the etag parameter when invoking the $app.restful method if the simultaneous editing of the resources in the client apps is not expected.

Cache-Control

The combination of the ETag and If-None-Match headers allows avoiding the redundant transfer of the resource data to the client. The elimination of the GET requests to the previously fetched resources will provide an enormous performance boost both to the client app and the backend application.

Universal Data Caching

The framework in the foundation of the applications created with Code On Time implements the universal data caching. The simple rules provided by developers can instruct the app to keep some of the fetched data in the application cache for the specified duration. The cached data is shared by the entire user base. This eliminates the database interactions and significantly reduces the response time of the backend application.

This is the example of the universal caching rules directing the Data Aquarium framework to keep the responses from the Suppliers and Categories controllers in the cache for 1 minute. The rules are defined in ~/app/touch-settings.json configuration file.

JSON
12345678910{
  "server": {
    "cache": {
      "rules": {
        "suppliers|categories": {
          "duration": 1
        }
      }
    }
}

By default, only the private caching of the RESTful resources is allowed on the client. Entity tags coupled with the 304 Not Modified status code instruct the clients to reuse the previously fetched data.

The built-in RESTful API Engine piggybacks on the same rules to cache the responses associated with the specific URLs in the application cache on the server. Furthermore it instructs the clients to keep the responses in their own cache for the same duration by including the max-age parameter in the Cache-Control header.

The engine navigates the segments of the requested URL and figures the data controller that will be producing the response. If there are caching rules, then the engine will try to locate the response in the cache. If the previous response is not available, then the physical execution of the request will take place with the subsequent placement to the server cache. In either case the max-age parameter is specified in the Cache-Control header.

The caching of the RESTful resource greatly reduces the response time of the fist fetch by the client. The max-age parameter eliminates the further requests completely for the specified duration.

The single page application SPA4 with CRUD fetches the lookup resources of categories and suppliers when the Edit Product form is displayed.

image7.png

The network log shows the requests executed by the client after the following sequence of user actions:

Firefox Developer Tools provide a complete list of HTTP requests executed by the web application. Other browsers may not provide the full picture of the network traffic between the client and the server.
Firefox Developer Tools provide a complete list of HTTP requests executed by the web application. Other browsers may not provide the full picture of the network traffic between the client and the server.

  • STEP 1: Click the Aniseed Syrup product in the product list.
    • The OPTION request is performed by the browser to validate the CORS policy of the backend application.
    • The /v2/products/3 resource is fetched with the response code 200.
    • The Edit Product form is constructed, which causes one PATCH request preceded by the OPTIONS check. The application generates the form using the metadata reported by the RESTful API Engine. The form layout is reused in the subsequent request.
    • A request to GET the suppliers fetches the SupplierID lookup data. It is preceded by the OPTIONS validation request.
    • A request to GET the categories fetches the CategoryID lookup data. It is also preceded by the OPTIONS validation request.
  • Click the Cancel button to close the form.
  • STEP 2: Click the same product row the second time.
    • The /v2/products/3 resource is fetched with the response code 200. The previous PATCH request to this resource has invalidated the entity tag in the view of the browser. The resource data is fetched again even though the data has not changed on the server.
    • Suppliers are fetched with the 200 status code directly from the client cache.
      max-age | local cache
    • Categories are fetched with the 200 status code directly from the client cache.
      max-age | local cache
  • Click the Cancel button to close the form.
  • STEP 3: Click the Aniseed Syrup product row the third time.
    • The /v2/products/3 resource is fetched from cache after the 304 status code is reported by the server indicating that the content has not changed. The request was processed on the server but no data was transmitted to the client.
      GET | 304 | local cache
    • Suppliers are fetched with the 200 status code directly from the client cache.
      max-age | local cache
    • Categories are fetched with the 200 status code directly from the client cache.
      max-age | local cache
  • Click the Cancel button to close the form.
  • STEP 4: Click the Alice Mutton product in the product list.
    • The /v2/products/17 resource is fetched with the response code 200. It is preceded by the OPTIONS validation request.
    • Suppliers are fetched with the 200 status code directly from the client cache.
      max-age | local cache
    • Categories are fetched with the 200 status code directly from the client cache.
      max-age | local cache
  • Click the Cancel button to close the form.
  • STEP 5: Wait for 30 minutes and click the Alice Mutton product the second time.
    • The /v2/products/17 resource is requested but not fetched. The status code of the response is 401 Unauthorized. The access token has expired.
    • Method $app.restful performs automatic refresh of the access token by executing the POST request to the /oauth2/v2/token resource.
    • Method $app.restful tries again to request the /v2/products/17 resource with a new access token. The resource is fetched with the response code 200.
    • Suppliers are fetched from cache after the 304 status code is reported by the server indicating that the resource content has not changed.
      GET | 304 | local cache
    • Categories are fetched from cache after the 304 status code is reported by the server indicating that the resource content has not changed.
      GET | 304 | local cache
  • Click the Cancel button to close the form.
  • STEP 6: Click the Alice Mutton product the third time.
    • The /v2/products/17 resource is fetched from cache after the 304 status code is reported by the server indicating that the resource content has not changed.
      GET | 304 | local cache
    • Suppliers are fetched with the 200 status code directly from the client cache.
      max-age | local cache
    • Categories are fetched with the 200 status code directly from the client cache.
      max-age | local cache

Web applications are taking full advantage of the HTTP Caching when working with the RESTful API Engine of applications created with Code On Time. Native client apps may also mirror the behavior of the browsers by inspecting the ETag and Cache-Control headers of responses and specifying the previously fetched entity tags in the If-None-Match header of requests.

Public API Key

If an API Key is specified in the URL and caching is enabled, then the Cache-Control header is set to the public value. It allows responses to be stored in the shared caches on the way to the client.

Here is the sample item Baked Goods from the categories collection resource. It includes the API Key aptly named “public” in the self hypermedia control and in the value of the picture field.

JSON
1234567891011121314{
    "_links": {
        "self": {
            "href": "/v2/public/categories/217"
        }
    },
    "categoryId": 217,
    "categoryName": "Baked Goods",
    "description": "Breads, cookies, pastries",
    "picture": "/v2/public/categories/MjE3L3BpY3R1cmUvag.jpeg",
    "fileName": "bread.jpeg",
    "contentType": "image/jpeg",
    "length": 13155
}

The API Key allows loading the resource directly in the browser window.

image5.png

Perform the following sequence of actions while monitoring the requests:

  • STEP 1: Open a new browser window, open Developer Tools, and enter the full URL of the picture directly in the address bar to see the /v2/public/categories/MjE3L3BpY3R1cmUvag.jpeg resource. Note that you file name may be different than the one in this example.
    • The image is fetched with the GET request.
      GET | 200
  • STEP 2: Place cursor in the browser address bar and press Enter key
    • The image is fetched from the local private cache. The network request is not performed.
      max-age | local cache
  • STEP 3: Place cursor in the browser address bar and press Enter key one more time.
    • Once more the image is fetched from the private cache without any help from the server.
      max-age | local cache
  • STEP 4: Click the Refresh button on the left-hand side of the address bar in the browser.
    • The browser makes a GET request to the server with the If-None-Match header set to the ETag of the image in the local cache.
    • The server responds with the 304 Not Modified status and nothing in the body.
    • The image is fetched from the local cache again.
      GET | 304 | local cache

Each successful request to GET the image is followed by a 404 error. Most browsers will attempt to load the "favico.ico" when an URL is entered in the address bar. The 404 response simply indicate that there is no icon in the application root on the server.
Each successful request to GET the image is followed by a 404 error. Most browsers will attempt to load the "favico.ico" when an URL is entered in the address bar. The 404 response simply indicate that there is no icon in the application root on the server.

RESTful API Engine will include the max-age and s-maxage parameters in the Cache-Control header if the path or query parameter specifies an API Key. The first parameter will tell the browsers to keep the response in the private cache. The second parameter will tell the shared caches and proxies to keep the public response for the specified duration. This will completely eliminate the handling of the incoming requests by the application server, since the shared cache server will have the response ready for the clients.

JSON and BLOB responses to the requests with the API keys  specified in the URLs are configured for public caching by the browsers, shared caches, and proxies.
JSON and BLOB responses to the requests with the API keys specified in the URLs are configured for public caching by the browsers, shared caches, and proxies.

Embedding With Caching

RESTful API Engine does not suffer from the under-fetching problem typical to the Level 2 REST APIs and below. Developers can fetch multiple resources in a single request thanks to the hypermedia.

Consider the products collection with the limit of 1 item presented below. The links supplierId and categoryId in the one and only item in the collection are marked with the embeddable property.

JSON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061{
    "_links": {
        "self": {
            "href": "/v2/products?limit=1"
        },
        "first": {
            "href": "/v2/products?page=0&limit=5"
        },
        "lookup-supplierId": {
            "href": "/v2/suppliers?count=true&fields=supplierId,companyName",
            "embeddable": true
        },
        "lookup-categoryId": {
            "href": "/v2/categories?count=true&fields=categoryId,categoryName",
            "embeddable": true
        },
        "create": {
            "href": "/v2/products",
            "method": "POST"
        },
        "do-something": {
            "href": "/v2/products/do-something",
            "method": "POST"
        },
        "report1": {
            "href": "/v2/products/report1"
        },
        "schema": {
            "href": "/v2/products?_schema=true&limit=1"
        }
    },
    "collection": [
        {
            "_links": {
                "self": {
                    "href": "/v2/products/17"
                },
                "supplierId": {
                    "href": "/v2/products/17/supplier-id",
                    "embeddable": true
                },
                "categoryId": {
                    "href": "/v2/products/17/category-id",
                    "embeddable": true
                }
            },
            "productId": 17,
            "productName": "Alice Mutton",
            "supplierId": 7,
            "supplierCompanyName": "Pavlova, Ltd.",
            "categoryId": 6,
            "categoryName": "Meat/Poultry",
            "quantityPerUnit": "20 - 1 kg tins",
            "unitPrice": 16.9000,
            "unitsInStock": 97,
            "unitsOnOrder": 23,
            "reorderLevel": 5,
            "discontinued": false
        }
    ]
}

Modify the URL of the request to have the _embed parameter with the value that enumerates the embeddable resources. Also add the _links=false parameter to remove the hypermedia controls from the output

/v2/products?limit=1&_embed=supplierId,categoryId&_links=false

The supplierId and categoryId field values in the collection item of the response are objects with the properties of the embedded resources. The engine automatically replaces the original fields with the corresponding embeddable resources.

JSON
1234567891011121314151617181920212223242526272829303132333435363738394041{
    "collection": [
        {
            "productId": 17,
            "productName": "Alice Mutton",
            "supplierId": {
                "supplierId": 7,
                "companyName": "Pavlova, Ltd.",
                "contactName": "Ian Devling",
                "contactTitle": "Marketing Manager",
                "Products2": "/v2/products/17/supplier-id/products2",
                "address": "74 Rose St. Moonie Ponds",
                "city": "Melbourne",
                "region": "Victoria",
                "postalCode": "3058",
                "country": "Australia",
                "phone": "(03) 444-2343",
                "fax": "(03) 444-6588",
                "products": "/v2/products/17/supplier-id/products"
            },
            "supplierCompanyName": "Pavlova, Ltd.",
            "categoryId": {
                "categoryId": 6,
                "categoryName": "Meat/Poultry",
                "description": "Prepared meats",
                "picture": "/v2/products/17/category-id/picture",
                "fileName": null,
                "contentType": null,
                "length": null,
                "products": "/v2/products/17/category-id/products"
            },
            "categoryName": "Meat/Poultry",
            "quantityPerUnit": "20 - 1 kg tins",
            "unitPrice": 16.9000,
            "unitsInStock": 97,
            "unitsOnOrder": 23,
            "reorderLevel": 5,
            "discontinued": false
        }
    ]
}

Take a note of the time it took to produce the response in a tool like Postman. Try execution of the same request a few times in the row. The response time will decrease thanks to the caching of Suppliers and Categories.

Remove the limit=1 parameter from the URL of the request and take a note of the time it takes to complete. About 80 product items will be included in the collection with the supplierId and categoryId fields resolved. Subsequent requests to the same URL are executing up to 40 times faster.

Add the Products data controller to the caching rules and you will discover further improvement in the response time, which was up to 200 times faster compared to the app configuration without caching in our own test.

Learn about fetching the data in the exact shape that is needed for the client app in the No More Under-Fetching Or Over-Fetching segment of the RESTful Workshop.

Latest Version

HTTP Caching does improve performance but makes it impossible to manage the data in the client apps. Universal Data Caching supports the exemptions. Developers can specify the roles that will not be affected by the caching rules. If the identity of the user associated with the access token or API Key has the role or scope of an exemption, then the engine will return the current version of the resource.

RESTful API Engine provides the means of reading the latest version of resources by users that are not exempt from caching.

Consider the categories collection resource in the next snippet.

The hypermedia control self has the max-age property letting the developers know that there is the caching rule set for this resource. The caching duration is 60 seconds. There is also the latest-version hypermedia control that provides access to the latest version of the resource content.

JSON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465{
    "_links": {
        "self": {
            "href": "/v2/categories",
            "max-age": 60
        },
        "latest-version": {
            "href": "/v2/categories/latest-version"
        },
        "first": {
            "href": "/v2/categories?page=0&limit=10"
        },
        "create": {
            "href": "/v2/categories",
            "method": "POST"
        },
        "schema": {
            "href": "/v2/categories?_schema=true"
        }
    },
    "collection": [
        {
            "_links": {
                "self": {
                    "href": "/v2/categories/217"
                }
            },
            "categoryId": 217,
            "categoryName": "Baked Goods",
            "description": "Breads, cookies, pastries",
            "picture": "/v2/categories/MjE3L3BpY3R1cmUvag.jpeg",
            "fileName": "bread.jpeg",
            "contentType": "image/jpeg",
            "length": 13155
        },
        {
            "_links": {
                "self": {
                    "href": "/v2/categories/1"
                }
            },
            "categoryId": 1,
            "categoryName": "Beverages",
            "description": "Soft drinks, coffees, teas, beers, and ales",
            "picture": "/v2/categories/1/picture",
            "fileName": null,
            "contentType": null,
            "length": null
        },
        {
            "_links": {
                "self": {
                    "href": "/v2/categories/2"
                }
            },
            "categoryId": 2,
            "categoryName": "Condiments",
            "description": "Sweet and savory sauces, relishes, spreads, and seasonings",
            "picture": "/v2/categories/2/picture",
            "fileName": null,
            "contentType": null,
            "length": null
        }
    ]
}

Here is the latest version of the categories collection resource. Note the latest-version segment is in the path of the hypermedia links. This magic suffix in the URL path will create a caching exemption that overrides the server caching rules.

JSON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061{
    "_links": {
        "self": {
            "href": "/v2/categories/latest-version"
        },
        "first": {
            "href": "/v2/categories/latest-version?page=0&limit=10"
        },
        "create": {
            "href": "/v2/categories/latest-version",
            "method": "POST"
        },
        "schema": {
            "href": "/v2/categories/latest-version?_schema=true"
        }
    },
    "collection": [
        {
            "_links": {
                "self": {
                    "href": "/v2/categories/217/latest-version"
                }
            },
            "categoryId": 217,
            "categoryName": "Baked Goods",
            "description": "Breads, cookies, pastries",
            "picture": "/v2/categories/latest-version/MjE3L3BpY3R1cmUvag.jpeg",
            "fileName": "bread.jpeg",
            "contentType": "image/jpeg",
            "length": 13155
        },
        {
            "_links": {
                "self": {
                    "href": "/v2/categories/1/latest-version"
                }
            },
            "categoryId": 1,
            "categoryName": "Beverages",
            "description": "Soft drinks, coffees, teas, beers, and ales",
            "picture": "/v2/categories/1/picture/latest-version",
            "fileName": null,
            "contentType": null,
            "length": null
        },
        {
            "_links": {
                "self": {
                    "href": "/v2/categories/2/latest-version"
                }
            },
            "categoryId": 2,
            "categoryName": "Condiments",
            "description": "Sweet and savory sauces, relishes, spreads, and seasonings",
            "picture": "/v2/categories/2/picture/latest-version",
            "fileName": null,
            "contentType": null,
            "length": null
        }
    ]
}

If the hypermedia controls are used in the client application, then it is trivial to switch between the cached and the latest version of a resource. It is easy to know when the cached data is being served by the backend application.

If the max-age parameter is specified in the self hypermedia control, then the data is cached. Use the latest-version control to access the latest resource content.
Thursday, April 28, 2022PrintSubscribe
Universal Data Caching

The app responsiveness drives the user satisfaction. The common bottleneck in achieving the quick response time is the backend application of the client app. Network requests and their processing by the backend are inherently slow.

Solution

Developers constantly look for ways to improve the performance of the server code when executing the database queries. Indexes, query hints, and beefier database servers can help a lot.

The best technique is to avoid “talking” to the database at any cost!

The Data Aquarium Framework in the foundation of applications created with Code On Time implements the controller-level caching. Developers identify the information that does not change frequently and specify the caching rules in ~/app/touch-settings.json configuration file.

Consider the following changes in the configuration of the backend application introduced in the RESTful Workshop. The server.cache.rules key defines the caching rules that will make the framework to reuse the previously constructed responses to the requests fetching the Categories and Suppliers data.

JSON
12345678910{
  "server": {
    "cache": {
      "rules": {
        "suppliers|categories": {
          "duration": 1
        }
      }
    }
}

The responses to the requests to fetch data will be placed in the server cache and will stay there for 1 minute. The framework looks up the previous response to a request in the cache. It performs the database queries only if the response is not found. The cache manager removes the stale responses automatically.

The names of the caching rules are the case-insensitive regular expressions evaluated against the data controller names. In this instance the single rule suppliers|categories covers both controllers. The duration property specifies the number of minutes the data will remain in the “cached” state. Set the duration to 1440 to keep the responses in the cache for 24 hours.

Let’s evaluate the impact of the universal caching by invoking repeatedly the Refresh command in the list suppliers of the backend application.

The "Refresh" option in the view selectors will fetch the fresh 
 data from the server. Refreshing requires a single roundtrip.
The "Refresh" option in the view selectors will fetch the fresh data from the server. Refreshing requires a single roundtrip.

The network monitor shows the first five Refresh requests taking between 11 and 20 milliseconds. The request #6 was performed when the caching rules were entered in touch-settings.json configuration file. The remaining requests show the consistent response time of 5 milliseconds. Caching provides a 70% improvement in the average response time in this particular application.

Chrome Developer Tools show the response time to the requests refreshing the list of suppliers.
Chrome Developer Tools show the response time to the requests refreshing the list of suppliers.

Numerous conditions will impact the performance of the database queries. For example, the response will take longer to produce if the database is on the remote server, the server is under heavy load, or the query is complex.

The cached responses will always have a consistent response time.

The categories and suppliers of the products are changed infrequently and are the perfect candidates for caching. The product inventory is changing frequently and shall not be cached.

Do not use caching with the data that cannot be reused. For example, the list of orders of a customer is unique. Its caching is not likely to improve the overall performance.

The cached response must not depend on the user identity or include the user-specific data.

Exemptions

Caching of the lookup data improves performance but introduces the unique challenge. How does one go about making changes to the cached data items? For example, backend administrators will have a hard time while entering the new suppliers or editing the existing ones. The frontend will keep presenting the “unchanged” information. It will take at least a minute for an application to start displaying the new data with the caching configuration presented above.

Caching rules can be enhanced with the exemptRoles and exemptScopes keys specifying the user roles or OAuth 2.0 scopes that are not affected by caching.

JSON
123456789101112{
  "server": {
    "cache": {
      "rules": {
        "suppliers|categories": {
          "duration": 1,
          "exemptRoles": "Administrators",
          "exemptScopes": "inventory.write"
        }
      }
    }
}

Multiple roles are separated with commas while multiple scopes are separated with spaces.

Caching lookup is not performed if the user identity matches either exemptRoles or exemptScopes.

Administrators will not feel the improved performance associated with caching since they are exempt from having their own requests to fetch suppliers and categories to undergo the “caching” treatment. On the other hand they will likely be the only users physically reading and writing the data that is retrieved from the cache for everybody else. Making edits to the data will have a familiar flow.

Labels: Caching
Thursday, April 21, 2022PrintSubscribe
BLOBs with RESTful API

Touch UI provides the following view of the Categories in the backend application. The Picture field contents can be downloaded, replaced, or deleted by users. The sketch pad allows making visual annotations on the image.

Images can be downloaded, replaced, or deleted. The BLOB input of Touch UI allows making the visual annotations on the image. The BLOB data is physically stored in the database or external storage.
Images can be downloaded, replaced, or deleted. The BLOB input of Touch UI allows making the visual annotations on the image. The BLOB data is physically stored in the database or external storage.

Every row in the Categories table contains a Binary Large Object (BLOB) in the Picture column. The data type of the column is image. Modern database engines allow storing the binary data of variable length directly in the tables. Applications created with Code On Time work with the BLOBs stored in the database or external storage, such as a folder of the file system or a container in Microsoft Azure.

The JSON data presented below is the categories singleton resource returned by the backend. There are five fields and lots of hypermedia. The picture field and products field have the API links as their values. The former corresponds to the physical column Categories.Picture. The latter is the products field defined in the Categories model.

RESTful API allows fetching the BLOB values, passing BLOBs in the CRUD requests, and replacing or deleting the BLOBs in the singleton resource fields.
JSON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152{
    "categoryId": 1,
    "categoryName": "Beverages",
    "description": "Soft drinks, coffees, teas, beers, and ales",
    "picture": "/v2/categories/1/picture",
    "products": "/v2/categories/1/products",
    "_links": {
        "self": {
            "href": "/v2/categories/1"
        },
        "up": {
            "href": "/v2/categories"
        },
        "collection": {
            "href": "/v2/categories?count=true"
        },
        "first": {
            "href": "/v2/categories?page=0&limit=10"
        },
        "products": {
            "href": "/v2/categories/1/products?count=true",
            "embeddable": true
        },
        "products-first": {
            "href": "/v2/categories/1/products?page=0&limit=10",
            "embeddable": true
        },
        "edit": {
            "href": "/v2/categories/1",
            "method": "PATCH"
        },
        "replace": {
            "href": "/v2/categories/1",
            "method": "PUT"
        },
        "replace-picture": {
            "href": "/v2/categories/1/picture",
            "method": "PUT"
        },
        "delete-picture": {
            "href": "/v2/categories/1/picture",
            "method": "DELETE"
        },
        "delete": {
            "href": "/v2/categories/1",
            "method": "DELETE"
        },
        "schema": {
            "href": "/v2/categories/1?_schema=true"
        }
    }
}

Fetching BLOBs

The BLOB can be fetched if the value of the singleton resource field is not equal to null. The link in the value identifies the resource followed by the name of the field. The original file name of the BLOB may follow the field name in the resource URL with certain configurations of the resource data controller.

Links in Values

The BLOB link in the sample data shown above consists of the /v2/categories/1 singleton resource URL and the picture field name concatenated together. An attempt to load the blob in the incognito or private browser mode will force the backend application to return the error. An authorization key or access token is required to read the resources known to the RESTful API.

RESTful API must be enabled explicitly in the application configuration. An authorization key or access token is required to access a resource.
RESTful API must be enabled explicitly in the application configuration. An authorization key or access token is required to access a resource.

Navigate to the backend application in the new browser window, sign in, and enter the same URL in the address bar. The image will be loaded successfully . Touch UI application creates an encrypted cookie to track the user identity. The BLOB request will be authenticated since the cookie is sent by the browser with each request.

RESTful API Engine identifies the user automatically when the host application is making a request. The cookie with the self-encrypted user access token provides the identity information.
RESTful API Engine identifies the user automatically when the host application is making a request. The cookie with the self-encrypted user access token provides the identity information.

Standalone single page applications and other HTTP clients will have to specify an authorization key or access token explicitly. Here is the blob fetched by Postman with the authorization key specified in the URL.

Postman shows the BLOB fetched from the resource with the authorization key specified in the "x-api-key" query parameter. RESTful API Engine requires an access token or authorization key to be specified explicitly by the external applications.
Postman shows the BLOB fetched from the resource with the authorization key specified in the "x-api-key" query parameter. RESTful API Engine requires an access token or authorization key to be specified explicitly by the external applications.

Most client applications will not be able to secure an authorization key or set up a cookie with the user identity. JavaScript client apps can establish a secure communication channel with the backend by taking advantage of the restful.js script. It is being used extensively in the standalone single page apps of the workshop.

Here is the customized SPA4 with RESTful Hypermedia and CRUD client app that fetches the images from the backend application in a secure fashion. An image appears in the Category cell when clicked by users.

The standalone single page application fetches the image of the product category on-demand when the category name is clicked. The app follows the hypermedia links to find the product category "picture" field value.
The standalone single page application fetches the image of the product category on-demand when the category name is clicked. The app follows the hypermedia links to find the product category "picture" field value.

The BLOB resource URL cannot be assigned to the src attribute of the image and must be fetched from the backend with the access token specified in the request header.

This is the modified version of the handleHypermedia function that does the image retrieval and assignment.

JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758function handleHypermedia(e) {
    var row = e.target.closest('[data-index]')
    if (row && products) {
        let tableCell = e.target.closest('td');
        if (tableCell.previousSibling != null && tableCell.previousSibling.previousSibling == null) {
            // the cell in the "Category" column was clicked
            $app.restful({
                url: products.collection[row.getAttribute("data-index")]._links.self,
                hypermedia: 'categoryId >>'
            }).then(obj => {
                $app.restful({
                    url: obj.picture
                }).then(blob => {
                    // create an object URL
                    let dataUrl = URL.createObjectURL(blob);
                    // create an image element and replace the cell content with it
                    let img = document.createElement('img');
                    img.src = dataUrl;            // set the image src
                    img.title = obj.categoryName; // set the image tooltip
                    img.style.maxWidth =          // set the maximum image width
                        tableCell.getBoundingClientRect().width - 12 + 'px';
                    // clear the cell content
                    tableCell.innerHTML = '';
                    // insert image in the cell
                    tableCell.appendChild(img);
                });
            });
        }
        else
            readProduct(row.getAttribute("data-index"));
    }
    else {
        var btn = e.target.closest('[data-hypermedia]');
        if (btn && products) {
            var hypermedia = btn.getAttribute('data-hypermedia');
            switch (hypermedia) {
                case 'new':
                    newProduct();
                    break;
                case 'create':
                    postProduct();
                    break;
                case 'edit':
                    patchProduct();
                    break;
                case 'delete':
                    deleteProduct();
                    break;
                case 'cancel':
                    hideForm();
                    break;
                default:
                    refreshProductList(products._links[hypermedia]);
                    break;
            }
        }
    }
}

The original implementation orchestrates the UI flow in the app by responding to the clicks on the product list rows and buttons. The Edit Product form is displayed when users click anywhere in the product row.

The extended code figures when the click occurs in the second column of the product table and goes on to fetch and display the category image in the table cell:

  • First, it invokes the $app.restful method. The self link of the clicked product item is specified in the url parameter. The hypermedia parameter tells the method to locate and fetch the categoryId hypermedia control in the singleton product resource. The obj argument in the then chain function will be the instance of the category object just like the one shown at the top of the tutorial.
  • The obj argument has the picture property. Another call of the $app.restful method will fetch the BLOB object from the url specified in the property value. The blob argument of the then chain function is the instance of the Blob object. The $app.restful method converts the server response into a Blob instance if the content type of the response is not JSON, XML, or YAML.
  • Finally, the Object URL is created from the Blob instance and the table cell contents are replaced with the image that has its src attribute set to base-64 encoded URL. The image is fitted to the cell boundaries and has its title set to the category name to serve as a tooltip.

The products resource collection items do not have the URL of the category picture. The display of the category image takes three requests:

  1. The product item is fetched from the self hypermedia link of the selected item in the product collection.
  2. The category is fetched from the categoryId hypermedia control of the product item.
  3. The BLOB is fetched from the resource specified by the picture field of the product category.
Client application discovers the missing data by following the hypermedia links.

A shrewd developer may figure the correct category image URL and reduce this discovery sequence to a single request:

JavaScript
123456789101112131415161718// the cell in the "Category" column was clicked
Let productItem = products.collection[row.getAttribute("data-index")];
$app.restful({
    url: '/v2/categories/' + productItem.categoryId + '/picture'
}).then(blob => {
    // create an object URL
    let dataUrl = URL.createObjectURL(blob);
    // create an image element and replace the cell content with it
    let img = document.createElement('img');
    img.src = dataUrl;            // set the image src
    img.title = productItem.categoryName; // set the image tooltip
    img.style.maxWidth =          // set the maximum image width
        tableCell.getBoundingClientRect().width - 12 + 'px';
    // clear the cell content
    tableCell.innerHTML = '';
    // insert image in the cell
    tableCell.appendChild(img);
});

The item in the product collection has the categoryId, which can be used to construct the URL of the categories resource singleton field without much difficulty.

The explicit resource URLs in the client application create a tight coupling between the client and the current implementation of the backend. This may lead to broken apps when the backend evolves over time.

If efficiency is important, then consider including the additional fields in the collections and singletons by denormalizing the data models. Avoid using the explicit URLs in the client code to ensure the application longevity and maintainability.

Public Links

Making dedicated requests to fetch an image from the server may seem exotic, but unavoidable if the images cannot be exposed to the world. BLOB objects may represent documents, images, video, and sound files, which frequently require limited access.

The product category image is not something that needs to be kept private. If that is the case, then an authorization key must become a part of the image URL specified in the src attribute of the img element. The job of fetching the image from the server can be outsourced to the browser and Document Object Model.

Let’s introduce the public authorization key in the ~/app/touch-settings.json configuration file of the backend application. Note that there is also the server.rest.acl section specifying the Access Control List of the API.

JSON
123456789101112131415161718192021222324252627282930313233343536{
  "ui": {
    "roles": "Administrators"
  },
  "server": {
    "rest": {
      "enabled": true,
      "output": {
        "json": {
          "indent": true
        }
      },
      "authorization": {
        "keys": [
          {
            "key": "94b79da7-e3a9-4152-9213-886f4c810bec",
            "user": "admin"
          },
          {
            "key": "public",
            "user": "user"
          }
        ]
      },
      "acl": {
        "categories": {
          "administrators": ".",
          "users": "GET"
        },
        "products|suppliers": {
          "administrators": "."
        }
      }
    }
  }
}

The word “public” is the fitting name for the public authorization key. In this example, the user account is associated with the public key.

The authorization key can be specified as the x-api-key or api_key query parameter. There is also a more elegant method of specifying the authorization key in the URL. The key may be included in the path right after the API version like this:

http://localhost:42439/v2/public/categories/8/picture

The URL with the public key in the path will fetch the image in the incognito mode if the access control list allows it.
The URL with the public key in the path will fetch the image in the incognito mode if the access control list allows it.

The word “public” in the key name has no special meaning and can be replaced with any mnemonic. Any authorization key discoverable by the third party is the public key.

Developers must configure an access control list (ACL) to limit the API exposure to the unauthenticated users that will consume the resources with a public authorization key.

The ACL in the configuration above will allow the identities with the Users role to perform the GET requests to the categories resources. Users in the Administrators role can perform requests to the resources of categories, products, and suppliers with any HTTP verb.

The names of the server.rest.acl key properties are the controller rules (regular expressions) that will be evaluated against the names of the data controllers. If the rule of the access control list is matched with the data controller name in the resource path, then the restful API will evaluate the identity rules (the properties of the matched controller rule). The property names are the application user roles or the OAuth 2.0 scopes. If the current user is in role or has the scope in the access token, then the engine will try to match the HTTP method or the custom action identifier with the identity rule value (also a regular expression). If there is no match, then the access is denied.

This is the code that will use the public authorization key to fetch the category images when the category name is clicked in the product grid.

JavaScript
12345678910111213// the cell in the "Category" column was clicked
let productItem = products.collection[row.getAttribute("data-index")];
let img = document.createElement('img');
img.src =                             
    'http://localhost:42439/v2/public/categories/' +  
    productItem.categoryId + '/picture'; // set the image src
img.title = productItem.categoryName;    // set the image tooltip
img.style.maxWidth =                     // set the maximum image width
    tableCell.getBoundingClientRect().width - 12 + 'px';
// clear the cell content
tableCell.innerHTML = '';
// insert image in the cell
tableCell.appendChild(img);

There are no explicit resource fetching requests. A simple assignment to the src property of the img element does the job. The only concern is the hard-coded URL pointing to the backend application.

File Names in Links

The Categories table does not store the attributes of the Picture image file. The original name, type, and length of the file is not known. The BLOB processing code of the framework detects the image data automatically and sets the appropriate content type and a generic file name in the response to the request to fetch a BLOB resource. The generic content type application/octet-stream will describe a BLOB that does not contain an image.

The resource /v2/public/categories/1/picture will fetch the BLOB data even though it appears to lack the physical file name.

Application framework relies on the optional utility fields to provide the extended information about the BLOB fields of a data controller. The field names FileName, ContentType, and Length will be treated as the default utility fields. Developers can be more specific and prefix the default utility field names with the name of the corresponding BLOB field.

Alter the Categories table by adding the FileName, ContentType, and Length columns to store the extended properties of the BLOB in the Picture column. Make sure that the BLOB and the utility columns allow null values.

image8.png

Select the backend application project on the start page of the app generator, choose Refresh, and proceed to incorporate the new database schema in the project metadata. Select the Model option in the summary, choose the Categories model, and select the new columns to include them in the entity data model.

Model Builder makes it easy to perform iterative enhancement of the data models when the database schema is changed.
Model Builder makes it easy to perform iterative enhancement of the data models when the database schema is changed.

Save the Categories model and proceed to generate the app. The new fields will become a part of the user interface. Begin adding a new category and observe how the File Name, Content Type, and Length are automatically set to the properties of the selected Picture file.

Touch UI automatically detects the "utility" fields and captures the File Name, Content Type, and Length of the binary data when a file is selected or dropped on the BLOB input field.
Touch UI automatically detects the "utility" fields and captures the File Name, Content Type, and Length of the binary data when a file is selected or dropped on the BLOB input field.

Changes to the model are also reflected in the RESTful API. Here is how the new categories singleton resource may appear to the apps and developers:

JSON
1234567891011121314151617181920212223242526{
    "_links": {
        "self": {
            "href": "/v2/public/categories/197"
        },
        "up": {
            "href": "/v2/public/categories"
        },
        "collection": {
            "href": "/v2/public/categories?count=true"
        },
        "first": {
            "href": "/v2/public/categories?page=0&limit=10"
        },
        "schema": {
            "href": "/v2/public/categories/197?_schema=true"
        }
    },
    "categoryId": 197,
    "categoryName": "Baked Goods",
    "description": "Breads, cookies, pastries",
    "picture": "/v2/public/categories/MTk3L3BpY3R1cmUvag.jpeg",
    "fileName": "bread.jpeg",
    "contentType": "image/jpeg",
    "length": 13155
}

Note that that picture value now has the file name but lacks the identifier of the singleton. The resource /v2/categories/197/picture transforms to /v2/categories/MTk3L3BpY3R1cmUvag.jpeg in the BLOB value. The URL is not affected by the original file name but inherits the file extension. The extension helps users to understand the nature of data.

The BLOB value with the authorization key in the path can be used in the public-facing websites. The extension of the file name gives a hint about the data type.
The BLOB value with the authorization key in the path can be used in the public-facing websites. The extension of the file name gives a hint about the data type.

If the “FileName” utility field is a part of the data controller, then its resource will have the BLOB value URL ending with the file name composed of its base64-url-encoded BLOB field name preceded by the previous path segment. The encoded file name will have the same extension as the original file.

ETag and 304 Not Modified

The headers of the response to the BLOB resource request will include the ETag (entity tag). The value is the unique hash of the BLOB content. Web browsers capture the ETag and send it with the subsequent requests to fetch the same resource.

The response of the BLOB resource request includes the ETag, Content-Type, Content-Disposition, and Content-Length headers. ETag value can be used to avoid the unnecessary transmission of the unchanged BLOB. Type, Disposition, and Length provide the client with the BLOB metadata.
The response of the BLOB resource request includes the ETag, Content-Type, Content-Disposition, and Content-Length headers. ETag value can be used to avoid the unnecessary transmission of the unchanged BLOB. Type, Disposition, and Length provide the client with the BLOB metadata.

The RESTful API Engine will compare the optional If-None-Match header of the request with the ETag of the resource. If both values are the same, then the engine will not send the BLOB data in the response. The body will be empty and the response status code will be set to 304 Not Modified. The client application will use the previously fetched resource data.

The "304 Not Modified" response includes the ETag of the resource that has not changed since the last fetch.
The "304 Not Modified" response includes the ETag of the resource that has not changed since the last fetch.

Web browsers and views automatically use the If-None-Match and ETag headers, which helps avoid the unnecessary transmission of data.

BLOBs in CRUD Requests

Binary field values can be specified in the CRUD operations with PUT, POST, and PATCH verbs. These HTTP methods correspond to the replace, create, and edit hypermedia controls in the resources of the RESTful API Engine.

The BLOB values can be found in the collection and singleton resources. The values of the BLOB fields are the API resources links.

Multipart Form Data

The content type multipart/form-data tells the server that the binary content is specified in the request body. The HTML forms with the “file” inputs are submitted with this content type in the “formdata” format by the web browsers and views.

Here is how the request with the binary data is constructed in Postman. This request will create a new category. Individual field values are listed as key-value pairs in the form-data tab of the request body. The picture field is configured as the file. The response body shows the new resource with the BLOB link in the picture value.

This is the "create" request in the Postman with the form-data fields in the request and the new category resource in the response. The "file" value is specified in the "picture" field of the request. The properties of the file are automatically captured by the RESTful API Engine in the "fileName", "contentType", and "length" fields of the new resource.
This is the "create" request in the Postman with the form-data fields in the request and the new category resource in the response. The "file" value is specified in the "picture" field of the request. The properties of the file are automatically captured by the RESTful API Engine in the "fileName", "contentType", and "length" fields of the new resource.

The plain JavaScript code that will create a category in the browser page is presented next. It uses the Fetch API to make the POST request. Note the use of the x-api-key header set to the authorization key of the admin user.

JavaScript
12345678910111213141516171819var myHeaders = new Headers();
myHeaders.append("x-api-key", "94b79da7-e3a9-4152-9213-886f4c810bec");

var formdata = new FormData();
formdata.append("categoryName", "Baked Goods");
formdata.append("description", "Breads, cookies, pastries");
formdata.append("picture", fileInput.files[0], "bread.jpeg");

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  body: formdata,
  redirect: 'follow'
};

fetch("http://localhost:42439/v2/categories", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));

The categoryName and description request fields can be specified in the single JSON object as the value of the unnamed field in the form-data. Its content type must be set to application/json. The binary fields of the resource must be specified as the named fields of the form-data. The order of the fields in the form-data is not important.

The non-binary fields of he request are specified in the form-data field without a name in the JSON format.
The non-binary fields of he request are specified in the form-data field without a name in the JSON format.

The raw HTTP request with the multipart/form-data content type is presented in the sample below. The request will create a new category. Client applications can configure HTTP requests on their own or use the libraries available with their chosen implementation technologies.

HTTP
1234567891011121314151617181920POST /v2/categories HTTP/1.1
Host: localhost:42439
x-api-key: 94b79da7-e3a9-4152-9213-886f4c810bec
Content-Length: 408
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="picture"; filename="bread.jpeg"
Content-Type: image/jpeg

(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name=""
Content-Type: application/json

{ 
   "categoryName": "Baked Goods",
   "description": "Breads, cookies, pastries"
}
----WebKitFormBoundary7MA4YWxkTrZu0gW

Method $app.restful

The standalone single page applications SPA4 and SPA5 are linking the lightweight external script with the definition of $app.restful method. The method automatically specifies the access token in the HTTP request headers to identify the user. The method refreshes the expired access token. An explicit resource URL or a hypermedia control can be specified in the url property of the method argument.

The restful.js resource of the backend application provides the standalone clients with a thin wrapper on top of the Fetch API.

The $app.restful method also detects the Blob fields specified in the body property of the argument. The request body with the Blob fields is automatically converted to an instance of the FormData object.

Let’s modify the CRUD-enabled version of SPA4 to create a category with the user-defined image in the picture field. Users will click on their avatar to activate the File Chooser prompt. The selected image file will become the picture of the new “Baked Goods” category. The name and the description of the category will be hard-coded for simplicity.

image5.png

Users can create new products in this category and observe the picture when the Category column is clicked in the product row.

image2.png

Add the following code at the bottom of the closure in the spa4.js. The code will attach the click handler to the avatar when the spa4.html page is loaded in the browser window.

JavaScript
123456789101112131415161718192021222324252627window.addEventListener('load', e => {
    // add 'click' event handler to the user avatar
    document.querySelector('#avatar').addEventListener('click', e => {
        // create a "file" input 
        var fileInput = document.createElement('input');
        fileInput.type = 'file';
        // wait for the "picture" file to be selected
        fileInput.addEventListener('change', e => {
            // create the "Baked Goods" category with the selected picture
            $app.restful({
                url: '/v2/categories',
                method: 'POST',
                body: {
                    categoryName: "Baked Goods",
                    description: "Breads, cookies, pastries",
                    picture: fileInput.files[0] // the picture image
                }
            }).then(newCategory => {
                // show the ID of the new category
                alert(newCategory.categoryId);
            }).catch(restfulException);

        });
        // display the File Chooser prompt in the browser 
        fileInput.click();
    });
});

The code dynamically creates an input of the file type and “clicks” it programmatically. The prompt to select a file is displayed. The change event will trigger on the input when a file is selected by the user. The $app.restful method executes the POST request with the categoryName, description, and picture specified in the body property of the method argument. The picture is set to the selected file.

Replacing and Deleting BLOB Values

The categories resource singleton hypermedia contains the replace-picture and delete-picture controls.

The blob image in the picture field of the resource can be replaced with the $app.restful method that has the replace-picture hypermedia object specified in its url property of the argument. The value field in the body property must be set to the new Blob or File value.

The delete-picture hypermedia control in the url property of the method argument will clear the BLOB value in the picture field.

The pair of the explicit url and method values can replace the hypermedia control in the $app.restful argument properties.

Next

Hypermedia in the resources provides instant documentation for developers and methods to transition the resource state or access the related data for the algorithms.

Learn to navigate the RESTful resources in the development tools and client apps.