The ReactAndGo project is used to show how to display Notifications with periodic requests to the backend and how to process rest requests in the backend in controllers and repositories.

React frontend

In the frontend, a dedicated worker is started after login that manages the notifications. The ‘initWebWorker(…)’ function of the LoginModal.tsx starts the worker and handles the tokens:

  const initWebWorker = async (userResponse: UserResponse) => {
    let result = null;
    if (!globalWebWorkerRefState) {
      const worker = new Worker(new URL('../webpush/dedicated-worker.js', import.meta.url));
      if (!!worker) {
        worker.addEventListener('message', (event: MessageEvent) => {
          //console.log(event.data);
          if (!!event?.data?.Token && event?.data.Token?.length > 10) {
            setGlobalJwtToken(event.data.Token);
          }
        });
        worker.postMessage({ jwtToken: userResponse.Token, newNotificationUrl: `/usernotification/new/${userResponse.Uuid}` } as MsgData);
        setGlobalWebWorkerRefState(worker);
        result = worker;
      }
    } else {
      globalWebWorkerRefState.postMessage({ jwtToken: userResponse.Token, newNotificationUrl: `/usernotification/new/${userResponse.Uuid}` } as MsgData);
      result = globalWebWorkerRefState;
    }
    return result;
  };

The React frontend uses the Recoil library for state management and checks if the ‘globalWebWorkerRefState’ exists. If not, the worker in ‘dedicated-worker.js’ gets created and the event listener for the Jwt tokens is created. The Jwt token is stored in a Recoil state to be used in all rest requests. Then the ‘postMessage(…)’ method of the worker is called to start the requests for the notifications. Then the worker is stored in the ‘globalWebWorkerRefState’ and the worker is returned.

The worker is developed in the dedicated-worker.ts file. The worker is needed as .js file. To have the help of Typescript, the worker is developed in Typescript and then turned into Javascript in the Typescript Playground. That saves a lot of time for me. The ‘refreshToken(…)’ function of the worker refreshes the Jwt tokens:

interface UserResponse {
  Token?: string
  Message?: string
}

let jwtToken = '';
let tokenIntervalRef: ReturnType<typeof setInterval>;
const refreshToken = (myToken: string) => {
  if (!!tokenIntervalRef) {
    clearInterval(tokenIntervalRef);
  }
  jwtToken = myToken;
  if (!!jwtToken && jwtToken.length > 10) {
    tokenIntervalRef = setInterval(() => {
      const requestOptions = {
        method: 'GET',
        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwtToken}` },
      };
      fetch('/appuser/refreshtoken', requestOptions).then(response => response.json() as UserResponse)
        .then(result => {
        if ((!result.Message && !!result.Token && result.Token.length > 10)) {
          //console.log('Token refreshed.');
          jwtToken = result.Token;
          /* eslint-disable-next-line no-restricted-globals */
          self.postMessage(result);
        } else {
          jwtToken = '';
          clearInterval(tokenIntervalRef);
        }
      });
    }, 45000);
  }
}

The ‘refreshToken(…)’ function first checks if another token interval has been started and stops it. Then the token is assigned and checked. If it passes the check a new interval is started to refresh the token every 45 seconds. The ‘requestOptions’ are created with the token in the ‘Authorization’ header field. Then the new token is retrieved with ‘fetch(…)’ and the response is checked, the token set, and it is posted to the EventListener in the LoginModal.tsx. If the Jwt token has not been received, the interval is stopped and the ‘jwtToken’ is set to an empty string.

The Eventlistener of the worker receives the token message and processes it as follows:

interface MsgData {
  jwtToken: string;
  newNotificationUrl: string;
}

let notificationIntervalRef: ReturnType<typeof setInterval>;
/* eslint-disable-next-line no-restricted-globals */
self.addEventListener('message', (event: MessageEvent) => {
  const msgData = event.data as MsgData;
  refreshToken(msgData.jwtToken);  
  if (!!notificationIntervalRef) {
    clearInterval(notificationIntervalRef);
  }
  notificationIntervalRef = setInterval(() => {
    if (!jwtToken) {
      clearInterval(notificationIntervalRef);
    }
    const requestOptions = {
      method: 'GET',
      headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwtToken}` },
    };
    /* eslint-disable-next-line no-restricted-globals */
    self.fetch(msgData.newNotificationUrl, requestOptions).then(result => result.json()).then(resultJson => {
      if (!!resultJson && resultJson?.length > 0) {
        /* eslint-disable-next-line no-restricted-globals */
        self.postMessage(resultJson);
        //Notification
        //console.log(Notification.permission);
        if (Notification.permission === 'granted') { 
          if(resultJson?.length > 0 && resultJson[0]?.Message?.length > 1 && resultJson[0]?.Title?.length > 1) {            
            for(let value of resultJson) {
            new Notification(value?.Title, {body: value?.Message});
            }
          }                
        }
      }
    });
  }, 60000);
});

The ‘addEventListener(…)’ method handles the ‘MessageEvent’ messages with the ‘MsgData’. The ‘jwtToken’ of the ‘MsgData’ is used to start the ‘refreshToken(…)’ function. Then it is checked to see if a notification interval has been started, and if so, it is stopped. Then a new interval is created that checks for new target matching gas prices every 60 seconds. The ‘jwtToken’ is checked, and if the check fails, the interval is stopped. Then the ‘requestOptions’ are created with the Jwt token in the ‘Authorization’ header field. Then ‘fetch(…)’ is used to retrieve the new matching gas price updates. Then the result json is checked and posted back to the EventListener in the LoginModal.tsx. With ‘Notification.permission’ the user gets asked for permission to send notifications, and ‘granted’ means he agreed. The data for the notification is checked, and the notification is sent with ‘new Notification(…)’.

Backend

To handle the frontend requests, the Go backend uses the Gin framework. The Gin framework provides the needed functions to handle Rest requests, like a router, context(url related stuff), Tls support, and Json handling. The route is defined in the basecontroller.go

func Start(embeddedFiles fs.FS) {
	router := gin.Default()
        ...
	router.GET("/usernotification/new/:useruuid", token.CheckToken, getNewUserNotifications)
        ...
	router.GET("/usernotification/current/:useruuid", token.CheckToken, getCurrentUserNotifications)
	router.StaticFS("/public", http.FS(embeddedFiles))
	router.NoRoute(func(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, "/public") })
	absolutePathKeyFile := strings.TrimSpace(os.Getenv("ABSOLUTE_PATH_KEY_FILE"))
	absolutePathCertFile := strings.TrimSpace(os.Getenv("ABSOLUTE_PATH_CERT_FILE"))
	myPort := strings.TrimSpace(os.Getenv("PORT"))
	if len(absolutePathCertFile) < 2 || len(absolutePathKeyFile) < 2 || len(myPort) < 2 {
		router.Run() // listen and serve on 0.0.0.0:3000
	} else {
		log.Fatal(router.RunTLS(":"+myPort, absolutePathCertFile, absolutePathKeyFile))
	}
}

The ‘Start’ function gets the embedded files for the ‘/public’ directory with the static frontend files. The line:

router.GET("/usernotification/new/:useruuid", token.CheckToken, getNewUserNotifications)

Creates the route ‘/usernotification/new/:useruuid’ with the ‘useruuid’ as parameter. The ‘CheckToken’ function in the token.go file handles the Jwt Token validation. The ‘getNewUserNotifications’ function in the in the uncontroller.go handles the requests.

The ‘getNewUserNotifications(…)’ function:

func getNewUserNotifications(c *gin.Context) {
	userUuid := c.Param("useruuid")
	myNotifications := notification.LoadNotifications(userUuid, true)
	c.JSON(http.StatusOK, mapToUnResponses(myNotifications))
}

...

func mapToUnResponses(myNotifications []unmodel.UserNotification) []unbody.UnResponse {
   var unResponses []unbody.UnResponse
   for _, myNotification := range myNotifications {
      unResponse := unbody.UnResponse{
         Timestamp: myNotification.Timestamp, UserUuid: myNotification.UserUuid, Title: myNotification.Title, 
            Message: myNotification.Message, DataJson: myNotification.DataJson,
      }
      unResponses = append(unResponses, unResponse)
   }
   return unResponses
}

The ‘getNewUserNotifications(…)’ function uses the Gin context to get the path parameter ‘useruuid’ and then calls the ‘LoadNotifications(…)’ function of the repository with it. The result is turned into ‘UserNotifications’ with the ‘mapToUnResponses(…)’ function, which sends only the data needed by the frontend. The Gin context is used to return the Http status OK and to marshal the ‘UserNotifications’ to Json.

The function ‘LoadNotifications(…)’ is in the unrepo.go file and loads the notifications from the database with the Gorm framework:

func LoadNotifications(userUuid string, newNotifications bool) []unmodel.UserNotification {
   var userNotifications []unmodel.UserNotification
   if newNotifications {
     database.DB.Transaction(func(tx *gorm.DB) error {
        tx.Where("user_uuid = ? and notification_send = ?", userUuid, !newNotifications)
           .Order("timestamp desc").Find(&userNotifications)
	for _, userNotification := range userNotifications {
	   userNotification.NotificationSend = true
	   tx.Save(&userNotification)
	}
	return nil
     })
   } else {
      database.DB.Transaction(func(tx *gorm.DB) error {
         tx.Where("user_uuid = ?", userUuid).Order("timestamp desc").Find(&userNotifications)
         var myUserNotifications []unmodel.UserNotification
         for index, userNotification := range userNotifications {
            if index < 10 {
	       myUserNotifications = append(myUserNotifications, userNotification)
	       continue
  	    }
	    tx.Delete(&userNotification)
          }
          userNotifications = myUserNotifications
          return nil
      })
    }
    return userNotifications
}

The ‘LoadNotifications(…)’ function checks if only new notifications are requested. Then a database transaction is created, and the new ‘UserNotifications'(notification.go) of the user file are selected, ordered newest first. The ‘send’ flag is set to true to mark them as no longer new, and the ‘UserNotifications’ are saved to the database. The transaction is then closed, and the notifications are returned.

If the current notifications are requested, a database transaction is opened and the ‘UserNotifications’ of the user are selected, ordered newest first. The first 10 notifications of the user are appended to the ‘myUserNotification’ slice, and the others are deleted from the database. Then the transaction is closed and the notifications are returned.

Conclusion

The frontend with React does work. React is a much smaller library than the Angular Framework and needs much more extra libraries like Recoil for state management. The features like interval are included in the Angular RxJs library. React has much fewer features and needs more additional libraries to achieve the same result. Angular is better for cases where the frontend needs more than basic features. React is better for simple frontends. A React frontend that grows to medium size will need more design and architecture work to be comparable to an Angular solution and might take more effort during development.

React is the kitplane that you have to assemble yourself. Angular is the plane that is rolled out of the factory.

The Go/Gin/Gorm backend works well. The Go language is much simpler than Java and makes reading it fast. Go can be learned in a relatively short amount of time and has strict types and a multi-threading concept that project Loom tries to add to Java. The Gin framework offers the features needed to develop the controllers and can be compared to the Spring Boot framework in features and ease of development. The Gorm framework offers the features needed to develop the repositories for database access and management and can be compared to the Spring Boot framework in terms of features and ease of development.

The selling point of Go is its lower memory consumption because it compiles to a binary and does not need a virtual machine. Java can catch up with Project Graal, but the medium- to large-sized examples have to be available and analyzed first. A decision can be based on developer skills, the amount of memory saved, and the state of project Graal.