How to develop a UI and an API at the same time, with no headaches
Tags: javascript, tutorial, productivity, programming
Published: 2023-03-08 10:12:12

You develop a WEB interface with ParcelJS, Angular, Vue, React... And of course, this interface will have to contact the API that you code in parallel. So you'll spend a little time fighting with CORS headers, having to configure the access point to take into account the production environment...

It's a shame, though. I am a strong supporter of the separation of view and control, and I think that a reverse-proxy is of course important in production. But, you are certainly coding your backend with a language and/or framework that knows how to serve WEB pages and static content.

This article shows the method with ParcelJS, but it will be easy enough to adapt it for any other bundler that allows to do automatic build. The main idea, as you will have understood, is not to use the built-in web server, but to make your backend application do it.

## The "bad" workflow

Before seeing the method I prefer to use, let me show you what I often see in many projects. The *bad* method...

Using ParcelJS, it makes it very easy to start developing an interface. Because `parcel` builds the application and use a "live reload" system. So, using `parcel` or `parcel serve` is simple.

This will make parcel start a WEB service on port `1234`, we just have to go to the page https://localhost:1234 and that's it.

But what we are interested in is that our WEB application connects to an API that we develop in Go or Python, for example.

Obviously, the API will listen to **another port**, and this gets complicated.

- Above all, the application should be configured to connect to the port of the API, in the development phase this is different from the production phase
- but even if we work locally, it is likely that your navigator prevents access to the API due to the management of "Cross Origin" headers (CORS).

And then, usually, I see project managers to install a reverse proxy, manipulating headers, making the Web UI to be able to connect the API to another port, and of course using a "settings" file that is changed from developmment environment to production.

That's a bit complex, isn't it?

> So what can we do? How to ease the developmment phase?
>
> We make our API the web server!

And you'll see that this will resolve both development and production builds. Yeah.

## Let's go for a little project

We start by creating a working directory, and we will do this, for example, in Go (we will see later for the Python with Flask)

```bash
mkdir -p ~/Project/demo-web1
cd !$
# we create the "ui" here
mkdir -p ui
cd !$
# then create the project
yarn init
yarn add --dev parcel rimraf
```

Of course, you can add TypeScript, ESLint, and whatever the tools you need.

Then, change the `package.json` file to set up scripts and the "source" entrypoint + add scripts:

```json
{
  //...
  "source": "src/index.html",
  //...
  "scripts": {
    "start": "rimraf dist .parcel-cache; parcel watch",
    "build": "rimraf dist .parcel-cache; parcel build"
  },
  //...
}
```

> It's important to change the "main" entry point to "source" and to point it on the "index.html" file.

OK, now create `src/index.html` page:

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Exemple</title>
    <script src="./main.js" type="module"></script>
  </head>
  <body>
    <h1>Exemple</h1>
    <div id="message"></div>
    <button id="button">Click me</button>
  </body>
</html>
```

Also, create this script (`src/main.js`):

```javascript
// call the /api/demo
function callApi() {
  fetch("/api/demo")
    .then((response) => response.json())
    .then((data) => {
      document.querySelector("#message").innerHTML = data.date;
    });
}

document.addEventListener("DOMContentLoaded", () => {
  document.querySelector("#button").addEventListener("click", callApi);
});
```

> Note that the call with `fetch` does not change the port, does not provide a particular url, it will be a call made on the same host. Since we will be serving the application via our API

So, at this time, you should have this structure:

```
└── ui
    ├── node_modules
    ├── package.json
    ├── src
    └── yarn.lock
```

**Did you realize that we don't use `parcel` or `parcel serve` but `parcel watch` ?**

> We now don't ask parcel to "serve" the interface, we only need to build tha application to `dist` folder. And we will serve this with our API/Server! That's all!

**It's time to create the API and server.**

Let's go up a level to `demo-web1` – we'll create the API here, but feel free to create a subdirectory if you like. You'll just have to change the path of the static folder.

In short!

```bash
# if you're still in the ui directory, go up
cd ..

# create the api
go mod init example
```

And here, create a `main.go` file that contains this:

```go
package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {

	// respond to /api/demo, send the date in json format
	http.HandleFunc("/api/demo", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		t := time.Now()
		w.WriteHeader(http.StatusOK)
		fmt.Fprintf(w, `{"date": "%s"}`, t.Format(time.RFC3339))
	})

	// serve the ui/dist directory as static
	http.Handle("/", http.FileServer(http.Dir("ui/dist")))

	// start the server
	log.Println("Starting server on port 8080")
	http.ListenAndServe(":8080", nil)
}
```

You should now have this project structure:

```
├── go.mod
├── main.go
└── ui
    ├── node_modules
    ├── package.json
    ├── src
    │   ├── index.html
    │   └── main.js
    └── yarn.lock
```

We can now access two endpoints:

- "`/api/demo`" returns the date (yes, it's fast-coded, we could use the `encoding/json` package, but that's just to show you)
- any other path will try to go to `ui/dist`

> `ui/dist` is where `parcel` will transpile our web interface - it doesn't exists yet, but it will be there.

OK, so now we can make sure that our UI is completed at each modification, on one side, and we launch the API on the other. To restart the Go application at each change, I often use `entr` which is very easy to use.

>  The "entr" command which is a standard package in Linux (install this with `dnf` or `apt`) and could be found for Mac. 

We will now run two commands:

- one that will compile the web interface code continuously, at each change
- another that will simply recompile the API

They have to run in parallel, so we will need two terminals for the moment.

```bash
# in the first terminal
cd ui && yarn run start

# in the second terminal
find . -name "*.go" | entr -r go run main.go
```

> For those who have `tmux` on their machine, a nice little trick is to run the following command. It can work perfectly well in a `Makefile` by the way
> ```
tmux new-session \
    "cd ui && yarn run start;" \
    split-window -h \
    "find . -name '*.go' | entr -r go run main.go"
```

Then go to http://localhost:8080, click on the button, and the date is displayed.

## Same with Flask

With Python and Flask (or Quart), you can do the same.

> You can, if you want, remove the `go.mod` and `main.go` file if you created them earlier.

OK, let's create a `virtualenv`, and install Flask.

```bash
# ensure you're not in "ui" directory then

python -mvenv venv
source venv/bin/activate
pip install flask
```

That's now OK – time to create our application.

Create the `main.py` file and type:

```python
from datetime import datetime

from flask import Flask, jsonify

app = Flask(__name__)

# use ui/dist as static
app.static_folder = "ui/dist"


@app.route("/api/demo")
def demo():
    """return the date and hour in json format"""
    date = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
    return jsonify(date)


@app.route("/")
@app.route("/<path:path>")
def index(path="index.html"):
    """Serve static files"""
    return app.send_static_file(path)


if __name__ == "__main__":
    app.run(debug=True)

```

One more time, start the UI generation, and now the Python service:

```bash
# in on terminal
cd ui && yarn run start

# in anther one (ensure you activated the venv before)
python main.py
```

And go to http://localhost:5000

> In debug mode, Flask reloads the server when Python sources changes. No need to use `entr`.

One more time, any changes in the UI is automatically compiled.

## And so, for deployment ?

Because the `parcel watch` process compiles everythin in `dist` directory, you will only need to `build` sources. The application, whatever if you use Go, Python, or any other language, will find the static files.

It's very cool when we want to use Docker (or Podman) in multistage build:

For the Go version, we can use a "scratch" image to only get UI and server binary. No need to get a Linux distribution.

```dockerfile
FROM node:alpine as js-build
COPY ./ui /app
RUN set -xe \
    cd /app; \
    yarn install; \
    yarn run build;

FROM golang:alpine as go-build
COPY ./ /app
RUN set -xe; \
    cd /app; \
    go mod tidy; \
    CGO_ENABLED=0 go build -o app.bin *.go

FROM scratch
COPY --from=js-build /app /app/ui
COPY --from=go-build /app/app.bin /app/app.bin
EXPOSE 8080
WORKDIR /app
CMD ["/app/app.bin"]
```

For Python, we can make approximately the same or use `gunicorn` to serve the application.

```dockerfile
FROM node:alpine as js-build
COPY ./ui /app
RUN set -xe \
    cd /app; \
    yarn install; \
    yarn run build;

FROM python:alpine
COPY ./ /app
COPY --from=js-build /app /app/ui
RUN set -xe; \
    pip install flask gunicorn

EXPOSE 8000
WORKDIR /app
CMD ["gunicorn", "main:app"]
```

## Conclusion

This way of doing things only works if you use a language or framework that is capable of serving static content, and if the WEB interface is coded with a tool that knows how to compile continuously without necessarily having a Web service.

Thus, we eliminate PHP... Because with it, it is necessary, in most cases, to use a Web server (nginx + FPM or Apache).

On the other hand, with Go, Python, Rust, or even Ruby, everything will be fine because these languages are stand-alone.

> I have seen, too often, many engineers, both beginners and experienced, launch a bundler in web server mode and struggle with configuration management + CORS in development mode.

The fear of using the API service to provide the pages is still big. Indeed, we can think that it would be good to use a "real" HTTP server to provide the WEB pages later (with a  reverse proxy for example)

However, to go fast, for a small project, or simply to propose a first iteration of the application, this method that I propose is more than enough. And it has the merit to be adapted to a reverse-proxy.

Note that it is not forbidden to serve, as I propose in this article, the static pages via the API. In fact, the rule that states that it is imperative to go through a reverse-proxy is a bit outdated, because modern languages and current fameworks are quite good in this exercise.

> Don't make me say what I didn't say, an upstream reverse-proxy is of course a good security guarantee. It will allow you to manage the TLS/SSL, the cache, some access rules or the load balancing!

But this reverse-proxy can perfectly call the API to have the static pages, to cache them, instead of reading them directly from the disk. This will also allow you to manage templating or content patching from your API. So, don't forbid yourself to do it.

In short, there is nothing wrong (really, nothing wrong at all) with letting the libraries provide the static pages, if only for a while.