Published on

Part 8 - OneHub Web

Introduction

Our canonical demo chat app - OneHub so far has:

  • A gRPC service/api for managing Topics and sending/receiving Messages
  • Restful bindings for http access
  • A CLI (in Cobra) for access via the terminal/command line

For a chat application a UI is a no-brainer. We will build a (super simple) Web UI using NextJS. The goal of this post is NOT to provide a cool UI or a deep tutorial on front end technologies - which is a technically very complex area. Instead the goal of this post is to:

  • Bring up a very simple web UI with standard frameworks/libraries
  • We will use NextJS for this. Not being being a front-end expert, ease of onboarding, community support and documentation/examples were key in making this decision.
  • Package the frontend as its own service in our docker-compose setup. This gives us ease of distribution (live reloading WILL be key).
  • Support development and production mode so that when we ready we can serve published artifacts as a static site as much as possible.
  • Demonstrate changes to our "web" layer needed to support a front-end.

Current Architecture (so far)

Below is our current architecture:

  • Currently the TopicService, MessageService and the gRPC Gateway all run within the same process (exposed in cmd/server.go). We have the flexibility to seperate them out if/when desired (a good trigger would be when we need to separate the data into their own datastores).
  • The grpc-gateway process accepts API requests (over http) and routes these to appropriate services (topics or messages).

Proposed Architecture

What we want is a new frontend (FE) service that only serves front end - either staticaly or as Server Side Rendered pages. And this FE service would make (CRUD) API calls to Topics, Messages and other entities. This leads to:

In this proposed architecture we have:

  • Our current grpc-gateway service running as before forwards API requests to the gRPC services.
  • We are adding a new dedicated frontend (service) that is responsible for rendering all pages/screens/views of our application as well as turning UI events (from the user) into updates on the various entities. The frontend service in our case is a simple NextJS based app (running on Port 4000). The app also primarily serves pages and static content (js, media, css etc) and also makes API calls to our gateway to present topics and messages.
  • We are also adding a new entity - the Router - that routes http requests to either the Frontend service or our grpc gateway based on whether the request is a API request or a view/page request. The Router is simply a reverse proxy that forwards client requests to the right service.

There are a few reasons for a reverse proxy:

  • Without a reverse proxy, the clients would have individually access the different points for different uses (API, vs FE vs others). For a reverse proxy we will be the popular Nginx and path based routing to our different services.
  • This problem is even worse when deployed to a public domain.
  • Our our frontend app accessing the grpc gateway on different ports would have resulted in CORS issues that are particularly painful to debug and fix - making local development slow!

To add nginx to our set of services we add the following config to our docker-compose.yml file (in the services section):

  nginx:
    image: nginx:latest
    ports:
      - 7080:80
    volumes:
      - ./configs/nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - onehub
      # - nextfe

Here we are exposing our nginx service that forwards requests to the onehub api server or the frontend server (nextfe) based on request paths (frontend service will be enabled in the next section). We are also mapping 3 files from our localfile system on to the container:

  • nginx.conf - For all routing, ssl configs and more

One noteable aspect of our nginx config is we are forwarding /api/v1/* to /v1/* on the api server for a clear seperation between API and non API requests. Obviously the api server is not aware of this translation.

Try out the APIs as before (with the new path and first setting the Host, Username and Password environment variables):

export OneHubHost=http://localhost:7080/api
export OneHubUsername=auser
export OneHubPassword=auser123
oh topics list
{
  "nextPageKey": "",
  "topics": [
    {
      "createdAt": "1970-01-01T00:00:00Z",
      "creatorId": "auser",
      "id": "t1",
      "name": "Computing",
      "updatedAt": "2023-08-13T04:31:01.298535Z",
      "users": {
        "1": true,
        "3": true
      }
    },
    {
      "createdAt": "1970-01-01T00:00:00Z",
      "creatorId": "auser",
      "id": "t2",
      "name": "Topic 2",
      "updatedAt": "2023-08-12T16:12:28.274891Z",
      "users": {}
    },
    {
      "createdAt": "1970-01-01T00:00:00Z",
      "creatorId": "auser",
      "id": "t3",
      "name": "Topic 3",
      "updatedAt": "2023-08-12T16:12:31.925136Z",
      "users": {}
    }
  ]
}

Setup a NextJS Frontend

We have built a very simple frontend webapp using NextJS and TypeScript. We wont be diving into a tutorial on NextJS. The NextJS documentation is quite brilliant as are the several other brilliant sources.

To start our very basic frontend, from the nextfe subfolder:

npm install
npm run dev

Pointing your browser here should show a basic UI like this:

Not much going on there clearly. This is currently running outside our docker compose environment. Let us move it into our docker compose setup so we have a single entry point.

Add the following Dockerfile in the nextfe folder:

nextfe/Dockerfile

# STAGE 1: A container with pnpm and python3 is required
FROM node:18-alpine as npm_base

WORKDIR /app

COPY package*.json /app

RUN npm install

# Finally, we run the NextJS app
CMD ["npm", "run", "dev"]

With this our base image now contains our npm package information as well as all the dependant modules installed. We will map the source code via Volumes so we can again have hot reloading when we change our front end sources.

Now add the following service to the docker-compose.yml file:

  nextfe:
    build: ./nextfe
    volumes:
       - ./nextfe:/app
       - /app/node_modules
       - /app/.next
    restart: always
    ports:
      - 4000:4000
    stdin_open: true

In our nginx.conf we have added the following route to forward requests to this app. This [redirect rule] forwards all requests on http://localhost:7080/nextfe/* internally to the nextfe service!

Now we can hit http://localhost:7080/nextfe for a single unified experience (and still have hot reloads etc).

Common User Flows

Let us go through a few flows to get started:

Signin

We are still using our fake users (where password == userid + "123") so you can log in and send messages as different users for a complete (and yet completely insecure) chat experience!

To login, click on signin and put in a user ID - This can be anything you desire as long as it is a combination of numbers and letters (only):

Since this user does not yet exist you will also be prompted for your Fullname to display in the future:

You are now ready to go ahead and chat. Create a couple of topics by clicking on the "New" button the Topics panel (on the left).

And after creating a few more topics:

And off you go sending messages (as different users):

Conclusion

In this part we created a simple web app (for reading/writing topics and messages) for our OneHub chat service.

This entailed some new ideas:

  • Building a simple web app and exposing it as a parallel service in our docker compose environment (we did not dive into specific front end stacks however).
  • Introducing nginx (as a router and a reverse proxy) to separate and forward FE and API traffic to respective services.

There are still a lot of things to add to this which we will address in subsequent posts:

  • Server to client pushes with WebSockets for data change notifications instead of polling or manually refreshing our pages.
  • Server-side rendering to offload (more) rendering onto a server. NextJS offers server side rendering however with a lockin into the Node ecosystem. We want to leverage our powerful and scalable Go dev ecosystem that is at our desposal. We will address this short coming and more.
  • Get to real users with proper authentication (Oauth, Logins etc)!
  • Bidirectional Integration with chat services (eg Slack, Teams, Discord etc) to send and receive messages.
  • (Easily maintainable/upgradable) Search indexes for powerful and efficient search capabilities.