Parcourir la source

Feat: add Docker+wasm examples (#309)

* Add a Docker+wasm sample application featuring a WasmEdge-based microservice, a MySQL database and an Nginx web server for frontend UI files.

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Add a logo to indicate Docker+wasm compatibility. Add project descriptions to README.

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Add the example for WasmEdge + Kafka / Redpanda + MySQL application to take messages from a queue and save into a database table.

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Add a SVG icon to indicate Docker + Wasm req

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update the docker compose files for the new Docker Desktop release

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Use the correct platform to be compatible with Docker Desktop 4.15

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Update README.md

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-kafka-mysql/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-kafka-mysql/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-kafka-mysql/etl/Dockerfile

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Update wasmedge-mysql-nginx/README.md

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

* Change the Nginx port to the default non-privileged 8090

Signed-off-by: Michael Yuan <michael@secondstate.io>

* My apologies. Need to correct the syntax for the Nginx port 8090.

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Remove commented lines

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Change wasi/wasm32 to wasi/wasm to conform with the latest spec

Signed-off-by: Michael Yuan <michael@secondstate.io>

* Update README.md

Co-authored-by: Michael Irwin <mikesir87@gmail.com>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>

Signed-off-by: Michael Yuan <michael@secondstate.io>
Signed-off-by: Michael Yuan <michael@michaelyuan.com>
Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Co-authored-by: Michael Irwin <mikesir87@gmail.com>
Michael Yuan il y a 3 ans
Parent
commit
e6b1d2755f

+ 4 - 0
README.md

@@ -20,6 +20,8 @@ These samples provide a starting point for how to integrate different services u
 
 <a href="https://docs.docker.com/desktop/dev-environments/"><img src="icon_devenvs.svg" alt="Use with Docker Dev Environments" height="30" align="top"/></a> Icon indicates Sample is compatible with [Docker Dev Environments](https://docs.docker.com/desktop/dev-environments/) in Docker Desktop version 4.10 or later.
 
+<a href="https://docs.docker.com/desktop/wasm/"><img src="icon_wasm.svg" alt="Docker + wasm" height="30" align="top"/></a> Icon indicates Sample is compatible with [Docker+Wasm](https://docs.docker.com/desktop/wasm/).
+
 - [`ASP.NET / MS-SQL`](aspnet-mssql) - Sample ASP.NET core application
 with MS SQL server database.
 - [`Elasticsearch / Logstash / Kibana`](elasticsearch-logstash-kibana) - Sample Elasticsearch, Logstash, and Kibana stack.
@@ -49,6 +51,8 @@ application with a Rust backend and a Postgres database.&nbsp;<a href="react-rus
 - [`React / Nginx`](react-nginx) - Sample React application with Nginx.&nbsp;<a href="react-nginx"><img src="icon_devenvs.svg" alt="Use with Docker Dev Environments" height="30" align="top"/></a>
 - [`Spring / PostgreSQL`](spring-postgres) - Sample Java application
 with Spring framework and a Postgres database.&nbsp;<a href="spring-postgres"><img src="icon_devenvs.svg" alt="Use with Docker Dev Environments" height="30" align="top"/></a>
+- [`WasmEdge / MySQL / Nginx`](wasmedge-mysql-nginx) - Sample Wasm-based web application with a static HTML frontend, using a MySQL (MariaDB) database. The frontend connects to a Wasm microservice written in Rust, that runs using the WasmEdge runtime.&nbsp;<a href="wasmedge-mysql-nginx"><img src="icon_wasm.svg" alt="Compatible with Docker+wasm" height="30" align="top"/></a>
+- [`WasmEdge / Kafka / MySQL`](wasmedge-kafka-mysql) - Sample Wasm-based microservice that subscribes to a Kafka (Redpanda) queue topic, and transforms and saves any incoming message into a MySQL (MariaDB) database.&nbsp;<a href="wasmedge-kafka-mysql"><img src="icon_wasm.svg" alt="Compatible with Docker+wasm" height="30" align="top"/></a>
 
 ## Single service samples
 

+ 13 - 0
icon_wasm.svg

@@ -0,0 +1,13 @@
+<svg id="WA-icon" xmlns="http://www.w3.org/2000/svg" width="144" height="111" viewBox="0 0 1440 1110">
+  <defs>
+    <style>
+      .cls-1 {
+        fill: #6f45f9;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <path id="shape_4" data-name="shape 4" class="cls-1" d="M445,280H651v7s0.437,32.383,29,49c30.515,17.752,62.137,4.583,72-4,10.362-9.017,22.2-23.3,23-45v-7H981V816H445V280ZM569,567h36l24,128,30-128h33l27,128,28-127h35L737,759H702L675,631,646,759H610ZM776,759l46-191h57l55,191H897l-12-42H820l-9,42H776Z" transform="translate(0.594 9.312)"/>
+  <path id="shape_1" data-name="shape 1" class="cls-1" d="M827,685.471l16-71h15l19,71H827Z" transform="translate(0.594 9.312)"/>
+  <path id="shape_3" data-name="shape 3" class="cls-1" d="M323,232.471l-122,54s-32,12.311-32,51v412s-4.458,38.615,29,54,497,221,497,221,22.409,11.15,46,1,116-51,116-51,14.081-5.669,14-22c-0.088-17.6-16.3-33.023-36-24s-96,43-96,43-17.573,10.1-37,2-449-198-449-198-24-11.257-24-40v-374a42.353,42.353,0,0,1,25-38c25.972-12.225,88-39,88-39s18-5.765,18-26C360,237.118,338.709,225.966,323,232.471Zm284-127,93-41s19.94-8.9,51,5,485,216,485,216,34,14.168,34,51v422s0.85,30.084-26,43-116,53-116,53-14.98,4.522-25-7-8.79-32.833,5-39,67-29,67-29l8-4s23-12.024,23-46v-369s1.61-26.142-28-39-437-194-437-194-21.48-9.931-40-2-77,34-77,34-22.162,7.988-33-8S584.324,115.515,607,105.471Z" transform="translate(0.594 9.312)"/>
+</svg>

+ 36 - 0
wasmedge-kafka-mysql/.docker/docker-compose.yml

@@ -0,0 +1,36 @@
+services:
+  redpanda:
+    image: docker.redpanda.com/vectorized/redpanda:v22.2.2
+    command:
+      - redpanda start
+      - --smp 1
+      - --overprovisioned
+      - --node-id 0
+      - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092
+      - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092
+      - --pandaproxy-addr 0.0.0.0:8082
+      - --advertise-pandaproxy-addr localhost:8082
+    ports:
+      - 8081:8081
+      - 8082:8082
+      - 9092:9092
+      - 9644:9644
+      - 29092:29092
+    volumes:
+      - ./kafka:/app
+  etl:
+    image: etl-kafka
+    platform: wasi/wasm
+    build:
+      context: etl
+    environment:
+      DATABASE_URL: mysql://root:whalehello@db:3306/mysql
+      KAFKA_URL: kafka://redpanda:9092/order
+      RUST_BACKTRACE: full
+      RUST_LOG: info
+    restart: unless-stopped
+    runtime: io.containerd.wasmedge.v1
+  db:
+    image: mariadb:10.9
+    environment:
+      MYSQL_ROOT_PASSWORD: whalehello

+ 117 - 0
wasmedge-kafka-mysql/README.md

@@ -0,0 +1,117 @@
+# Compose sample application
+
+![Compatible with Docker+Wasm](../icon_wasm.svg)
+
+This sample demonstrates a WebAssembly (Wasm) microservice written in Rust. It subscribes to a Kafka queue topic on a Redpanda server, and then transforms and saves each message into a MySQL (MariaDB) database table. The microservice is compiled into Wasm and runs in the WasmEdge runtime, which is a secure and lightweight alternative to natively compiled Rust apps in Linux containers.
+
+## Use with Docker Development Environments
+
+You will need a version of Docker Desktop or Docker CLI with Wasm support.
+
+* [Install Docker Desktop + Wasm (Beta)](https://docs.docker.com/desktop/wasm/)
+* [Install Docker CLI + Wasm](https://github.com/chris-crone/wasm-day-na-22/tree/main/server)
+
+## WasmEdge server with Redpanda and MySQL database
+
+Project structure:
+
+```
+.
++-- compose.yml
+|-- etl
+    |-- Dockerfile
+    |-- Cargo.toml
+    +-- src
+        |-- main.rs
+|-- kafka
+    |-- order.json
+|-- db
+    |-- db-password.txt
+```
+
+The [compose.yml](compose.yml) is as follows.
+
+```yaml
+services:
+  redpanda:
+    image: docker.redpanda.com/vectorized/redpanda:v22.2.2
+    command:
+      - redpanda start
+      - --smp 1
+      - --overprovisioned
+      - --node-id 0
+      - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092
+      - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092
+      - --pandaproxy-addr 0.0.0.0:8082
+      - --advertise-pandaproxy-addr localhost:8082
+    ports:
+      - 8081:8081
+      - 8082:8082
+      - 9092:9092
+      - 9644:9644
+      - 29092:29092
+    volumes:
+      - ./kafka:/app
+
+  etl:
+    image: etl-kafka
+    build:
+      context: etl
+      platforms:
+        - wasi/wasm32
+    environment:
+      DATABASE_URL: mysql://root:whalehello@db:3306/mysql
+      KAFKA_URL: kafka://redpanda:9092/order
+      RUST_BACKTRACE: full
+      RUST_LOG: info
+    restart: unless-stopped
+    runtime: io.containerd.wasmedge.v1
+
+  db:
+    image: mariadb:10.9
+    environment:
+      MYSQL_ROOT_PASSWORD: whalehello
+```
+
+The compose file defines an application with three services `redpanda`, `etl` and `db`. The `redpanda` service is a Kafka-compatible messaging server that produces messages in a queue topic. The `etl` service, in the WasmEdge container that subscribes to the queue topic and receives incoming messages. Each incoming message is parsed and stored in the `db` MySQL (MariaDB) database server.
+
+## Deploy with docker compose
+
+```bash
+$ docker compose up -d
+...
+ ⠿ Network wasmedge-kafka-mysql_default       Created                      0.1s
+ ⠿ Container wasmedge-kafka-mysql-redpanda-1  Created                      0.3s
+ ⠿ Container wasmedge-kafka-mysql-etl-1       Created                      0.3s
+ ⠿ Container wasmedge-kafka-mysql-db-1        Created                      0.3s
+```
+
+## Expected result
+
+```bash
+$ docker compose ps
+NAME                              COMMAND                  SERVICE             STATUS              PORTS
+wasmedge-kafka-mysql-db-1         "docker-entrypoint.s…"   db                  running             3306/tcp
+wasmedge-kafka-mysql-etl-1        "kafka.wasm"             etl                 running
+wasmedge-kafka-mysql-redpanda-1   "/entrypoint.sh 'red…"   redpanda            running             0.0.0.0:8081-8082->8081-8082/tcp, :::8081-8082->8081-8082/tcp, 0.0.0.0:9092->9092/tcp, :::9092->9092/tcp, 0.0.0.0:9644->9644/tcp, :::9644->9644/tcp, 0.0.0.0:29092->29092/tcp, :::29092->29092/tcp
+```
+
+After the application starts, 
+log into the Redpanda container and send a message to the queue topic `order` as follows.
+
+```bash
+$ docker compose exec redpanda /bin/bash
+redpanda@1add2615774b:/$ cd /app
+redpanda@1add2615774b:/app$ cat order.json | rpk topic produce order
+Produced to partition 0 at offset 0 with timestamp 1667922788523.
+```
+
+To see the data in the database container, you can use the following commands.
+
+```bash
+$ docker compose exec db /bin/bash
+root@c97c472db02e:/# mysql -u root -pwhalehello mysql
+mysql> select * from orders;
+... ...
+```
+

+ 36 - 0
wasmedge-kafka-mysql/compose.yml

@@ -0,0 +1,36 @@
+services:
+  redpanda:
+    image: docker.redpanda.com/vectorized/redpanda:v22.2.2
+    command:
+      - redpanda start
+      - --smp 1
+      - --overprovisioned
+      - --node-id 0
+      - --kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092
+      - --advertise-kafka-addr PLAINTEXT://redpanda:29092,OUTSIDE://redpanda:9092
+      - --pandaproxy-addr 0.0.0.0:8082
+      - --advertise-pandaproxy-addr localhost:8082
+    ports:
+      - 8081:8081
+      - 8082:8082
+      - 9092:9092
+      - 9644:9644
+      - 29092:29092
+    volumes:
+      - ./kafka:/app
+  etl:
+    image: etl-kafka
+    platform: wasi/wasm
+    build:
+      context: etl
+    environment:
+      DATABASE_URL: mysql://root:whalehello@db:3306/mysql
+      KAFKA_URL: kafka://redpanda:9092/order
+      RUST_BACKTRACE: full
+      RUST_LOG: info
+    restart: unless-stopped
+    runtime: io.containerd.wasmedge.v1
+  db:
+    image: mariadb:10.9
+    environment:
+      MYSQL_ROOT_PASSWORD: whalehello

+ 1 - 0
wasmedge-kafka-mysql/db/db-password.txt

@@ -0,0 +1 @@
+whalehello

+ 17 - 0
wasmedge-kafka-mysql/etl/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "kafka"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.65"
+mega_etl = {git = "https://github.com/second-state/MEGA.git"}
+tokio_wasi = {version = '1.21', features = ["rt", "macros"]}
+env_logger = "0.9"
+log  = "0.4"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+http_req_wasi  = "0.10"
+lazy_static = "1.4.0"

+ 27 - 0
wasmedge-kafka-mysql/etl/Dockerfile

@@ -0,0 +1,27 @@
+# syntax=docker/dockerfile:1
+FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase
+RUN <<EOT bash
+    set -ex
+    apt-get update
+    apt-get install -y \
+        git \
+        clang
+    rustup target add wasm32-wasi
+EOT
+# This line installs WasmEdge including the AOT compiler
+RUN curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
+
+FROM buildbase AS build
+COPY Cargo.toml .
+COPY src ./src 
+# Build the Wasm binary
+RUN --mount=type=cache,target=/usr/local/cargo/git/db \
+    --mount=type=cache,target=/usr/local/cargo/registry/cache \
+    --mount=type=cache,target=/usr/local/cargo/registry/index \
+    cargo build --target wasm32-wasi --release
+# This line builds the AOT Wasm binary
+RUN /root/.wasmedge/bin/wasmedgec target/wasm32-wasi/release/kafka.wasm kafka.wasm
+
+FROM scratch
+ENTRYPOINT [ "kafka.wasm" ]
+COPY --link --from=build /kafka.wasm /kafka.wasm

+ 58 - 0
wasmedge-kafka-mysql/etl/src/main.rs

@@ -0,0 +1,58 @@
+use mega_etl::{async_trait, Pipe, Transformer, TransformerError, TransformerResult};
+
+use serde::{Deserialize, Serialize};
+#[derive(Serialize, Deserialize, Debug)]
+struct Order {
+    order_id: i32,
+    product_id: i32,
+    quantity: i32,
+    amount: f32,
+    shipping: f32,
+    tax: f32,
+    shipping_address: String,
+}
+
+#[async_trait]
+impl Transformer for Order {
+    async fn transform(inbound_data: &Vec<u8>) -> TransformerResult<Vec<String>> {
+        let s = std::str::from_utf8(&inbound_data)
+            .map_err(|e| TransformerError::Custom(e.to_string()))?;
+        let order: Order = serde_json::from_str(String::from(s).as_str())
+            .map_err(|e| TransformerError::Custom(e.to_string()))?;
+        log::info!("{:?}", &order);
+        let mut ret = vec![];
+        let sql_string = format!(
+            r"INSERT INTO orders VALUES ({:?}, {:?}, {:?}, {:?}, {:?}, {:?}, {:?}, CURRENT_TIMESTAMP);",
+            order.order_id,
+            order.product_id,
+            order.quantity,
+            order.amount,
+            order.shipping,
+            order.tax,
+            order.shipping_address,
+        );
+        dbg!(sql_string.clone());
+        ret.push(sql_string);
+        Ok(ret)
+    }
+
+    async fn init() -> TransformerResult<String> {
+        Ok(String::from(
+            r"CREATE TABLE IF NOT EXISTS orders (order_id INT, product_id INT, quantity INT, amount FLOAT, shipping FLOAT, tax FLOAT, shipping_address VARCHAR(50), date_registered TIMESTAMP DEFAULT CURRENT_TIMESTAMP);",
+        ))
+    }
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> anyhow::Result<()> {
+    env_logger::init();
+
+    // can use builder later
+    let database_uri = std::env::var("DATABASE_URL")?;
+    let kafka_uri = std::env::var("KAFKA_URL")?;
+    let mut pipe = Pipe::new(database_uri, kafka_uri).await;
+
+    // This is async because this calls the async transform() function in Order
+    pipe.start::<Order>().await?;
+    Ok(())
+}

+ 1 - 0
wasmedge-kafka-mysql/kafka/order.json

@@ -0,0 +1 @@
+{"order_id": 1,"product_id": 12,"quantity": 2,"amount": 56.0,"shipping": 15.0,"tax": 2.0,"shipping_address": "Mataderos 2312"}

+ 25 - 0
wasmedge-mysql-nginx/.docker/docker-compose.yml

@@ -0,0 +1,25 @@
+services:
+  frontend:
+    image: nginx:alpine
+    ports:
+      - 8090:80
+    volumes:
+      - ./frontend:/usr/share/nginx/html
+
+  backend:
+    image: demo-microservice
+    platform: wasi/wasm
+    build:
+      context: backend/
+    ports:
+      - 8080:8080
+    environment:
+      DATABASE_URL: mysql://root:whalehello@db:3306/mysql
+      RUST_BACKTRACE: full
+    restart: unless-stopped
+    runtime: io.containerd.wasmedge.v1
+
+  db:
+    image: mariadb:10.9
+    environment:
+      MYSQL_ROOT_PASSWORD: whalehello

+ 125 - 0
wasmedge-mysql-nginx/README.md

@@ -0,0 +1,125 @@
+# Compose sample application
+
+![Compatible with Docker+Wasm](../icon_wasm.svg)
+
+This sample demonstrates a web application with a WebAssembly (Wasm) microservice, written in Rust. The Wasm microservice is an HTTP API connected to a MySQL (MariaDB) database. The API is invoked via from JavaScript in a web interface serving static HTML. The microservice is compiled into WebAssembly (Wasm) and runs in the WasmEdge Runtime, a secure and lightweight alternative to natively compiled Rust apps in Linux containers. Checkout [this article](https://blog.logrocket.com/rust-microservices-server-side-webassembly/) or [this video](https://www.youtube.com/watch?v=VSqMPFr7SEs) to learn how the Rust code in this microservice works.
+
+## Use with Docker Development Environments
+
+You will need a version of Docker Desktop or Docker CLI with Wasm support.
+
+* [Install Docker Desktop + Wasm (Beta)](https://docs.docker.com/desktop/wasm/)
+* [Install Docker CLI + Wasm](https://github.com/chris-crone/wasm-day-na-22/tree/main/server)
+
+## WasmEdge server with Nginx proxy and MySQL database
+
+Project structure:
+
+```
+.
++-- compose.yml
+|-- backend
+    +-- Dockerfile
+    |-- Cargo.toml
+    |-- src
+        +-- main.rs
+|-- frontend
+    +-- index.html
+    |-- js
+        +-- app.js
+|-- db
+    +-- orders.json
+    |-- update_order.json
+```
+
+The [compose.yml](compose.yml) file:
+
+```yaml
+services:
+  frontend:
+    image: nginx:alpine
+    ports:
+      - 8090:80
+    volumes:
+      - ./frontend:/usr/share/nginx/html
+
+  backend:
+    image: demo-microservice
+    build:
+      context: backend/
+      platforms:
+        - wasi/wasm32
+    ports:
+      - 8080:8080
+    environment:
+      DATABASE_URL: mysql://root:whalehello@db:3306/mysql
+      RUST_BACKTRACE: full
+    restart: unless-stopped
+    runtime: io.containerd.wasmedge.v1
+
+  db:
+    image: mariadb:10.9
+    environment:
+      MYSQL_ROOT_PASSWORD: whalehello
+```
+
+The compose file defines an application with three services `frontend`, `backend` and `db`. The `frontend` is a simple Nginx server that hosts static web pages that access the `backend` web service, in the WasmEdge container, via HTTP port 8080. When deploying the application, docker compose maps port 8090 of the `frontend` service container to port 8090 of the host as specified in the file. Make sure that ports 8090 and 8080 on the host are not already being used.
+
+## Deploy with docker compose
+
+```bash
+$ docker compose up -d
+...
+ ⠿ Network wasmedge-mysql-nginx_default       Created
+ ⠿ Container wasmedge-mysql-nginx-db-1        Created
+ ⠿ Container wasmedge-mysql-nginx-frontend-1  Created
+ ⠿ Container wasmedge-mysql-nginx-backend-1   Created
+```
+
+## Expected result
+
+```bash
+$ docker compose ps
+NAME                              COMMAND                  SERVICE             STATUS              PORTS
+wasmedge-mysql-nginx-backend-1    "order_demo_service.…"   backend             running             0.0.0.0:8080->8080/tcp, :::8080->8080/tcp
+wasmedge-mysql-nginx-db-1         "docker-entrypoint.s…"   db                  running             3306/tcp
+wasmedge-mysql-nginx-frontend-1   "/docker-entrypoint.…"   frontend            running             0.0.0.0:8090->80/tcp, :::8090->80/tcp
+```
+
+After the application starts, go to `http://localhost:8090` in your web browser to display the web frontend. 
+
+### Using the API with `curl`
+
+As an alternative to the web frontend, you can use `curl` to interact with the WasmEdge API directly (the `backend` service).
+
+When the WasmEdge web service receives a GET request to the `/init` endpoint, it would initialize the database with the `orders` table.
+
+```bash
+curl http://localhost:8080/init
+```
+
+When the WasmEdge web service receives a POST request to the `/create_order` endpoint, it extracts the JSON data from the POST body and inserts an `Order` record into the database table.
+To insert multiple records, use the `/create_orders` endpoint and POST a JSON array of `Order` objects:
+
+```bash
+curl http://localhost:8080/create_orders -X POST -d @db/orders.json
+```
+
+When the WasmEdge web service receives a GET request to the `/orders` endpoint, it gets all rows from the `orders` table and return the result set in a JSON array in the HTTP response.
+
+```bash
+curl http://localhost:8080/orders
+```
+
+When the WasmEdge web service receives a POST request to the `/update_order` endpoint, it extracts the JSON data from the POST body and update the `Order` record in the database table that matches the `order_id` in the input data.
+
+```bash
+curl http://localhost:8080/update_order -X POST -d @db/update_order.json
+```
+
+When the WasmEdge web service receives a GET request to the `/delete_order` endpoint, it deletes the row in the `orders` table that matches the `id` GET parameter.
+
+```bash
+curl http://localhost:8080/delete_order?id=2
+```
+

+ 13 - 0
wasmedge-mysql-nginx/backend/Cargo.toml

@@ -0,0 +1,13 @@
+[package]
+name = "order_demo_service"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0"
+serde_json = "1.0"
+serde = { version = "1.0", features = ["derive"] }
+url = "2.3"
+mysql_async_wasi = "0.30"
+hyper_wasi = { version = "0.15", features = ["full"] }
+tokio_wasi = { version = "1", features = ["io-util", "fs", "net", "time", "rt", "macros"] }

+ 29 - 0
wasmedge-mysql-nginx/backend/Dockerfile

@@ -0,0 +1,29 @@
+# syntax=docker/dockerfile:1
+
+FROM --platform=$BUILDPLATFORM rust:1.64 AS buildbase
+WORKDIR /src
+RUN <<EOT bash
+    set -ex
+    apt-get update
+    apt-get install -y \
+        git \
+        clang
+    rustup target add wasm32-wasi
+EOT
+# This line installs WasmEdge including the AOT compiler
+RUN curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
+
+FROM buildbase AS build
+COPY Cargo.toml .
+COPY src ./src 
+# Build the Wasm binary
+RUN --mount=type=cache,target=/usr/local/cargo/git/db \
+    --mount=type=cache,target=/usr/local/cargo/registry/cache \
+    --mount=type=cache,target=/usr/local/cargo/registry/index \
+    cargo build --target wasm32-wasi --release
+# This line builds the AOT Wasm binary
+RUN /root/.wasmedge/bin/wasmedgec target/wasm32-wasi/release/order_demo_service.wasm order_demo_service.wasm
+
+FROM scratch
+ENTRYPOINT [ "order_demo_service.wasm" ]
+COPY --link --from=build /src/order_demo_service.wasm /order_demo_service.wasm

+ 237 - 0
wasmedge-mysql-nginx/backend/src/main.rs

@@ -0,0 +1,237 @@
+use hyper::service::{make_service_fn, service_fn};
+use hyper::{Body, Method, Request, Response, StatusCode, Server};
+pub use mysql_async::prelude::*;
+pub use mysql_async::*;
+use std::convert::Infallible;
+use std::net::SocketAddr;
+use std::result::Result;
+use std::collections::HashMap;
+use serde::{Deserialize, Serialize};
+
+fn get_url() -> String {
+    if let Ok(url) = std::env::var("DATABASE_URL") {
+        let opts = Opts::from_url(&url).expect("DATABASE_URL invalid");
+        if opts
+            .db_name()
+            .expect("a database name is required")
+            .is_empty()
+        {
+            panic!("database name is empty");
+        }
+        url
+    } else {
+        "mysql://root:pass@127.0.0.1:3306/mysql".into()
+    }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+struct Order {
+    order_id: i32,
+    product_id: i32,
+    quantity: i32,
+    amount: f32,
+    shipping: f32,
+    tax: f32,
+    shipping_address: String,
+}
+
+impl Order {
+    fn new(
+        order_id: i32,
+        product_id: i32,
+        quantity: i32,
+        amount: f32,
+        shipping: f32,
+        tax: f32,
+        shipping_address: String,
+    ) -> Self {
+        Self {
+            order_id,
+            product_id,
+            quantity,
+            amount,
+            shipping,
+            tax,
+            shipping_address,
+        }
+    }
+}
+
+async fn handle_request(req: Request<Body>, pool: Pool) -> Result<Response<Body>, anyhow::Error> {
+    match (req.method(), req.uri().path()) {
+        (&Method::GET, "/") => Ok(Response::new(Body::from(
+            "The valid endpoints are /init /create_order /create_orders /update_order /orders /delete_order",
+        ))),
+
+        // Simply echo the body back to the client.
+        (&Method::POST, "/echo") => Ok(Response::new(req.into_body())),
+
+        // CORS OPTIONS
+        (&Method::OPTIONS, "/init") => Ok(response_build(&String::from(""))),
+        (&Method::OPTIONS, "/create_order") => Ok(response_build(&String::from(""))),
+        (&Method::OPTIONS, "/create_orders") => Ok(response_build(&String::from(""))),
+        (&Method::OPTIONS, "/update_order") => Ok(response_build(&String::from(""))),
+        (&Method::OPTIONS, "/delete_order") => Ok(response_build(&String::from(""))),
+        (&Method::OPTIONS, "/orders") => Ok(response_build(&String::from(""))),
+        
+        (&Method::GET, "/init") => {
+            let mut conn = pool.get_conn().await.unwrap();
+            "DROP TABLE IF EXISTS orders;".ignore(&mut conn).await?;
+            "CREATE TABLE orders (order_id INT, product_id INT, quantity INT, amount FLOAT, shipping FLOAT, tax FLOAT, shipping_address VARCHAR(20));".ignore(&mut conn).await?;
+            drop(conn);
+            Ok(response_build("{\"status\":true}"))
+        }
+
+        (&Method::POST, "/create_order") => {
+            let mut conn = pool.get_conn().await.unwrap();
+
+            let byte_stream = hyper::body::to_bytes(req).await?;
+            let order: Order = serde_json::from_slice(&byte_stream).unwrap();
+
+            "INSERT INTO orders (order_id, product_id, quantity, amount, shipping, tax, shipping_address) VALUES (:order_id, :product_id, :quantity, :amount, :shipping, :tax, :shipping_address)"
+                .with(params! {
+                    "order_id" => order.order_id,
+                    "product_id" => order.product_id,
+                    "quantity" => order.quantity,
+                    "amount" => order.amount,
+                    "shipping" => order.shipping,
+                    "tax" => order.tax,
+                    "shipping_address" => &order.shipping_address,
+                })
+                .ignore(&mut conn)
+                .await?;
+
+            drop(conn);
+            Ok(response_build("{\"status\":true}"))
+        }
+
+        (&Method::POST, "/create_orders") => {
+            let mut conn = pool.get_conn().await.unwrap();
+
+            let byte_stream = hyper::body::to_bytes(req).await?;
+            let orders: Vec<Order> = serde_json::from_slice(&byte_stream).unwrap();
+
+            "INSERT INTO orders (order_id, product_id, quantity, amount, shipping, tax, shipping_address) VALUES (:order_id, :product_id, :quantity, :amount, :shipping, :tax, :shipping_address)"
+                .with(orders.iter().map(|order| {
+                    params! {
+                        "order_id" => order.order_id,
+                        "product_id" => order.product_id,
+                        "quantity" => order.quantity,
+                        "amount" => order.amount,
+                        "shipping" => order.shipping,
+                        "tax" => order.tax,
+                        "shipping_address" => &order.shipping_address,
+                    }
+                }))
+                .batch(&mut conn)
+                .await?;
+
+            drop(conn);
+            Ok(response_build("{\"status\":true}"))
+        }
+
+        (&Method::POST, "/update_order") => {
+            let mut conn = pool.get_conn().await.unwrap();
+
+            let byte_stream = hyper::body::to_bytes(req).await?;
+            let order: Order = serde_json::from_slice(&byte_stream).unwrap();
+
+            "UPDATE orders SET product_id=:product_id, quantity=:quantity, amount=:amount, shipping=:shipping, tax=:tax, shipping_address=:shipping_address WHERE order_id=:order_id"
+                .with(params! {
+                    "product_id" => order.product_id,
+                    "quantity" => order.quantity,
+                    "amount" => order.amount,
+                    "shipping" => order.shipping,
+                    "tax" => order.tax,
+                    "shipping_address" => &order.shipping_address,
+                    "order_id" => order.order_id,
+                })
+                .ignore(&mut conn)
+                .await?;
+
+            drop(conn);
+            Ok(response_build("{\"status\":true}"))
+        }
+
+        (&Method::GET, "/orders") => {
+            let mut conn = pool.get_conn().await.unwrap();
+
+            let orders = "SELECT * FROM orders"
+                .with(())
+                .map(&mut conn, |(order_id, product_id, quantity, amount, shipping, tax, shipping_address)| {
+                    Order::new(
+                        order_id,
+                        product_id,
+                        quantity,
+                        amount,
+                        shipping,
+                        tax,
+                        shipping_address,
+                    )},
+                ).await?;
+
+            drop(conn);
+            Ok(response_build(serde_json::to_string(&orders)?.as_str()))
+        }        
+        
+        (&Method::GET, "/delete_order") => {
+            let mut conn = pool.get_conn().await.unwrap();
+
+            let params: HashMap<String, String> = req.uri().query().map(|v| {
+                url::form_urlencoded::parse(v.as_bytes()).into_owned().collect()
+            }).unwrap_or_else(HashMap::new);
+            let order_id = params.get("id");
+
+            "DELETE FROM orders WHERE order_id=:order_id"
+                .with(params! { "order_id" => order_id, })
+                .ignore(&mut conn)
+                .await?;
+
+            drop(conn);
+            Ok(response_build("{\"status\":true}"))
+        }
+
+        // Return the 404 Not Found for other routes.
+        _ => {
+            let mut not_found = Response::default();
+            *not_found.status_mut() = StatusCode::NOT_FOUND;
+            Ok(not_found)
+        }
+    }
+}
+
+// CORS headers
+fn response_build(body: &str) -> Response<Body> {
+    Response::builder()
+        .header("Access-Control-Allow-Origin", "*")
+        .header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+        .header("Access-Control-Allow-Headers", "api,Keep-Alive,User-Agent,Content-Type")
+        .body(Body::from(body.to_owned()))
+        .unwrap()
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+    let opts = Opts::from_url(&*get_url()).unwrap();
+    let builder = OptsBuilder::from_opts(opts);
+    // The connection pool will have a min of 5 and max of 10 connections.
+    let constraints = PoolConstraints::new(5, 10).unwrap();
+    let pool_opts = PoolOpts::default().with_constraints(constraints);
+    let pool = Pool::new(builder.pool_opts(pool_opts));
+
+    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
+    let make_svc = make_service_fn(|_| {
+        let pool = pool.clone();
+        async move {
+            Ok::<_, Infallible>(service_fn(move |req| {
+                let pool = pool.clone();
+                handle_request(req, pool)
+            }))
+        }
+    });
+    let server = Server::bind(&addr).serve(make_svc);
+    if let Err(e) = server.await {
+        eprintln!("server error: {}", e);
+    }
+    Ok(())
+}

+ 25 - 0
wasmedge-mysql-nginx/compose.yml

@@ -0,0 +1,25 @@
+services:
+  frontend:
+    image: nginx:alpine
+    ports:
+      - 8090:80
+    volumes:
+      - ./frontend:/usr/share/nginx/html
+
+  backend:
+    image: demo-microservice
+    platform: wasi/wasm
+    build:
+      context: backend/
+    ports:
+      - 8080:8080
+    environment:
+      DATABASE_URL: mysql://root:whalehello@db:3306/mysql
+      RUST_BACKTRACE: full
+    restart: unless-stopped
+    runtime: io.containerd.wasmedge.v1
+
+  db:
+    image: mariadb:10.9
+    environment:
+      MYSQL_ROOT_PASSWORD: whalehello

+ 47 - 0
wasmedge-mysql-nginx/db/orders.json

@@ -0,0 +1,47 @@
+[
+  {
+    "order_id": 1,
+    "product_id": 12,
+    "quantity": 2,
+    "amount": 56.0,
+    "shipping": 15.0,
+    "tax": 2.0,
+    "shipping_address": "Mataderos 2312"
+  },
+  {
+    "order_id": 2,
+    "product_id": 15,
+    "quantity": 3,
+    "amount": 256.0,
+    "shipping": 30.0,
+    "tax": 16.0,
+    "shipping_address": "1234 NW Bobcat"
+  },
+  {
+    "order_id": 3,
+    "product_id": 11,
+    "quantity": 5,
+    "amount": 536.0,
+    "shipping": 50.0,
+    "tax": 24.0,
+    "shipping_address": "20 Havelock"
+  },
+  {
+    "order_id": 4,
+    "product_id": 8,
+    "quantity": 8,
+    "amount": 126.0,
+    "shipping": 20.0,
+    "tax": 12.0,
+    "shipping_address": "224 Pandan Loop"
+  },
+  {
+    "order_id": 5,
+    "product_id": 24,
+    "quantity": 1,
+    "amount": 46.0,
+    "shipping": 10.0,
+    "tax": 2.0,
+    "shipping_address": "No.10 Jalan Besar"
+  }
+]

+ 9 - 0
wasmedge-mysql-nginx/db/update_order.json

@@ -0,0 +1,9 @@
+{
+    "order_id": 3,
+    "product_id": 12,
+    "quantity": 2,
+    "amount": 56.0,
+    "shipping": 15.0,
+    "tax": 2.0,
+    "shipping_address": "123 Main Street"
+}

+ 102 - 0
wasmedge-mysql-nginx/frontend/index.html

@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Demo App</title>
+  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous" />
+  <style>.d-none { display: none; }</style>
+</head>
+<body class="mb-5">
+  <div class="container mt-5">
+    <div id="app-loading-display">
+      <h1>Loading...</h1>
+    </div>
+  
+    <div id="order-display" class="d-none">
+      <h1>Welcome to the Demo!</h1>
+      <p>This application is served using nginx for the website, Wasm for the backend, and MariaDB for the database.</p>
+
+      <div id="order-empty-text" class="d-none">
+        <em>There are currently no orders to display!</em>
+      </div>
+
+      <table id="order-table" class="d-none table table-striped">
+        <thead>
+          <tr>
+            <th>Id</th>
+            <th>Product Id</th>
+            <th>Quantity</th>
+            <th>Amount</th>
+            <th>Shipping</th>
+            <th>Tax</th>
+            <th>Address</th>
+            <th></th>
+          </tr>
+        </thead>
+        <tbody></tbody>
+      </table>
+
+      <hr />
+
+      <div id="add-order-wrapper" class="d-none row">
+        <div class="col-6">
+         <div class="accordion" id="accordionExample">
+            <div class="accordion-item">
+              <h2 class="accordion-header" id="addOrderHeading">
+                <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#addOrder" aria-controls="addOrder">
+                  Add an order
+                </button>
+              </h2>
+              
+              <div id="addOrder" class="accordion-collapse collapse p-3" aria-labelledby="addOrderHeading" data-bs-parent="#accordionExample">
+                <form id="add-order-form">
+                  <div class="mb-3">
+                    <label for="order-id" class="form-label">Order Id</label>
+                    <input type="number" required class="form-control" id="order-id" aria-describedby="orderIdHelp">
+                    <div id="orderIdHelp" class="form-text">The ID of the order</div>
+                  </div>
+                  <div class="mb-3">
+                    <label for="product-id" class="form-label">Product Id</label>
+                    <input type="number" required class="form-control" id="product-id" aria-describedby="productIdHelp">
+                    <div id="productIdHelp" class="form-text">The ID of the product</div>
+                  </div>
+                  <div class="mb-3">
+                    <label for="quantity" class="form-label">Quantity</label>
+                    <input type="number" required class="form-control" id="quantity" aria-describedby="quantityHelp">
+                    <div id="quantityHelp" class="form-text">How many of the product?</div>
+                  </div>
+                  <div class="mb-3">
+                    <label for="amount" class="form-label">Amount</label>
+                    <input type="number" required class="form-control" id="amount" aria-describedby="amountHelp">
+                    <div id="amountHelp" class="form-text">The total amount</div>
+                  </div>
+                  <div class="mb-3">
+                    <label for="tax" class="form-label">Tax</label>
+                    <input type="number" required class="form-control" id="tax" aria-describedby="taxHelp">
+                    <div id="taxHelp" class="form-text">The total amount of tax</div>
+                  </div>
+                  <div class="mb-3">
+                    <label for="shippingAmount" class="form-label">Shipping Amount</label>
+                    <input type="number" required class="form-control" id="shippingAmount" aria-describedby="shippingAmountHelp">
+                    <div id="shippingAmountHelp" class="form-text">The total amount for shipping</div>
+                  </div>
+                  <div class="mb-3">
+                    <label for="shippingAddress" class="form-label">Shipping Address</label>
+                    <input type="text" required class="form-control" id="shippingAddress" aria-describedby="addressHelp">
+                    <div id="addressHelp" class="form-text">Where to send the order</div>
+                  </div>
+
+                  <input type="submit" class="btn btn-success" value="Add Order" />
+                </form>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+  </div>
+
+  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script>
+  <script type="text/javascript" src="/js/app.js"></script>
+</body>
+</html>

+ 135 - 0
wasmedge-mysql-nginx/frontend/js/app.js

@@ -0,0 +1,135 @@
+(function() {
+  let orders = null;
+  const appLoadingEle = document.getElementById("app-loading-display");
+  const orderWrapperEle = document.getElementById("order-display");
+  const orderEmptyTextEle = document.getElementById("order-empty-text");
+  const orderTableEle = document.getElementById("order-table");
+  const orderTableBodyEle = document.querySelector("#order-table tbody");
+  const addOrderEle = document.getElementById("add-order-wrapper");
+  const addOrderForm = document.getElementById("add-order-form");
+
+  const orderIdField = document.getElementById("order-id");
+  const productIdField = document.getElementById("product-id");
+  const quantityField = document.getElementById("quantity");
+  const amountField = document.getElementById("amount");
+  const taxField = document.getElementById("tax");
+  const shippingField = document.getElementById("shippingAmount");
+  const shippingAddressField = document.getElementById("shippingAddress");
+
+  function fetchOrders() {
+    fetch("http://localhost:8080/orders")
+      .then(r => r.json())
+      .then(r => orders = r)
+      .then(renderOrders)
+      .catch((e) => {
+        init();
+      });
+  }
+
+  function init() {
+    fetch("http://localhost:8080/init")
+      .then(() => fetchOrders())
+      .catch((e) => displayError(e));
+  }
+
+  function renderOrders() {
+    appLoadingEle.classList.add("d-none");
+    orderWrapperEle.classList.remove("d-none");
+    addOrderEle.classList.remove("d-none");
+
+    if (orders.length === 0) {
+      orderEmptyTextEle.classList.remove("d-none");
+      orderTableEle.classList.add("d-none");
+      return;
+    }
+
+    orderEmptyTextEle.classList.add("d-none");
+    orderTableEle.classList.remove("d-none");
+
+    while (orderTableBodyEle.firstChild) {
+      orderTableBodyEle.removeChild(orderTableBodyEle.firstChild);
+    }
+
+    orders.forEach((order) => {
+      const orderId = order.order_id;
+
+      const row = document.createElement("tr");
+
+      row.appendChild(createCell(order.order_id));
+      row.appendChild(createCell(order.product_id));
+      row.appendChild(createCell(order.quantity));
+      row.appendChild(createCell(order.amount));
+      row.appendChild(createCell(order.shipping));
+      row.appendChild(createCell(order.tax));
+      row.appendChild(createCell(order.shipping_address));
+
+      const actionCell = document.createElement("td");
+
+      const deleteButton = document.createElement("button");
+      deleteButton.classList.add(...["btn","btn-sm","btn-danger"]);
+      deleteButton.innerText = "Delete";
+
+      deleteButton.addEventListener("click", (e) => {
+        e.preventDefault();
+        deleteOrder(orderId);
+      });
+
+      actionCell.appendChild(deleteButton);
+
+      row.appendChild(actionCell);
+
+      orderTableBodyEle.appendChild(row);
+    });
+  }
+
+  function createCell(contents) {
+    const cell = document.createElement("td");
+    cell.innerText = contents;
+    return cell;
+  }
+
+  function deleteOrder(orderId) {
+    fetch(`http://localhost:8080/delete_order?id=${orderId}`)
+      .then(() => fetchOrders());
+  }
+
+  function displayError(err) {
+    alert("Error:" + err);
+  }
+
+  function onAddFormSubmit(e) {
+    e.preventDefault();
+
+    const data = {
+      order_id : parseFloat(orderIdField.value),
+      product_id : parseFloat(productIdField.value),
+      quantity : parseFloat(quantityField.value),
+      amount : parseFloat(amountField.value),
+      shipping : parseFloat(shippingField.value),
+      tax : parseFloat(taxField.value),
+      shipping_address : shippingAddressField.value,
+    };
+
+    fetch("http://localhost:8080/create_order", {
+      method: "POST",
+      body: JSON.stringify(data),
+      headers: { "Content-type": "application/json" },
+    }).then(() => fetchOrders())
+      .then(() => resetAddOrderForm());
+
+    alert("Order added");
+  }
+
+  function resetAddOrderForm() {
+    orderIdField.value = "";
+    productIdField.value = "";
+    quantityField.value = "";
+    amountField.value = "";
+    shippingField.value = "";
+    taxField.value = "";
+    shippingAddressField.value = "";
+  }
+
+  fetchOrders();
+  addOrderForm.addEventListener("submit", onAddFormSubmit);
+})();