Migrating Next.js App to GO × Templ × HTMX

backend May 4, 2024

Intro

Recently, I just rewrite one of my application Stashbin from Next.js to GO. Though my main motivation of this migration was to learn GO and experimenting with HTMX. I also aiming to reduce the resource usage of my application and simplify the deployment process. Initially, Stashbin codebase are split into two seperate repository, one for the frontend that uses Next.js and another for the backend that already uses GO. The backend repository is just a REST API responsible for storing and retreiving data from the database.

With the migration, I want to combine both frontend and backend into a single repository and serve both the API and the web application from single application. This will simplify the deployment process and reduce the resource usage of the application.

I also mentioned on my previous post that I’m already so frustrated with React further more Next.js. I’m not saying that they were bad, I just don’t enjoy working with it anymore.

So, without further ado, let’s get started.

The Migration

Planning and Preparation

In this project, I’m planning to merge the backend and frontend into a single monolith application. Both of them will be served from a single web server in Go. I’m planning to use several libraries to get the project done:

  • Echo for the web server.
  • sqlx for the database driver.
  • Templ for the templating engine. Although Go already have a decent templating engine, I’m planning to use Templ because it’s more powerful and flexible. I really like this library and I’m planning to use it in my future projects.

Migrating the Backend

Migrating the backend is quite simple as the previous backend is already written in Go. There will be only a few adjustments that I need to make since I’m planning to drop the ORM and use sqlx instead. All i need is to change the query done by the ORM to basic sql query.

Migrating the Frontend

This is the most challenging part of the migration. I need to rewrite the frontend from Next.js to Go (Templ syntax). I also use HTMX to replace React’s functionality, communicating with the backend, and updating the DOM.

Here’s some example of the Templ syntax for the main page:

package view

import "github.com/mrmissx/stashbin/view/layout"
import "github.com/mrmissx/stashbin/view/icons"

templ Home() {
	@layout.BaseLayout() {
		<form hx-post="/api/document" hx-trigger="keydown[keyCode === 83 && (ctrlKey || metaKey)]">
			@layout.Header() {
				@icons.Save()
			}
			<main>
				<textarea
					id="editor"
					name="content"
					placeholder="Write or paste, save and share the link..."
				></textarea>
			</main>
		</form>
		@layout.Footer()

		<script>
			document.addEventListener("keydown", (e) => {
				if (e.keyCode == 83 && (navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey)) {
					// prevent default Ctrl+S or Cmd+S
					e.preventDefault();
					return
				}
			}, false);
		</script>
	}
}

With Templ, I can just import a layout and component like what React does with the @ syntax. I also use the hx-* attribute to communicate with the backend using HTMX API. In build time, Templ will generate a Go code that represents the HTML code above and compile it to the final binary file.

Deployment

With the new monolith application, I can simplify the docker container deployments. I just need to build the application and run the binary file. I don’t need to build the frontend and backend separately and deploy them to different containers.

FROM golang:1.21-alpine AS build

WORKDIR /app

RUN apk update && \
    apk add --no-cache nodejs npm

COPY go.mod go.sum package.json package-lock.json ./

RUN go mod download && \
    go mod verify && \
    go install github.com/a-h/templ/cmd/templ@latest \
    npm install

COPY . .

RUN templ generate && \  # generate the Go code from the Templ syntax
    npm run build        # run the tailwindcss build

RUN go build -o stashbin  # build the final binary


FROM alpine

WORKDIR /app

COPY --from=build /app/stashbin .
COPY --from=build /app/public ./public

CMD ["./stashbin"]

This is the Dockerfile that I use to build the application. I use a multi-stage build to build the application and copy the final binary with all assets to the final image. We will get the most minimal image size as possible.

Conclusion

With this migration, I’m able to reduce the resource usage of my application, simplify the deployment process also improve the developer experience. Here’s some of the improvements that I can see:

Image Size

The most significant improvement can be seen immediately from the docker image size. With just single codebase in GO I can bring the image size down from >100MB to just 16MB. Both of them are already the best optimization using docker build step without shiping the build dependencies.

Old image size
microservice app
New monolith image size
monolith app

Resource Usage

With the new monolith application, I can reduce the resource usage of Stashbin especially the memory usage. The old application uses around 150MB of memory while the new GO application only uses 4MB of memory.

There are many other minor improvements that I can’t mention here like build time, developer experience, deployment speed, etc. I’m happy with the result and I’m looking forward to experiment more with GO and HTMX. If you want to contribute or reach me out, you can check the repository here.

See you in the next post! Cheers!

Tags

Gaung Ramadhan

Software Engineer. Creating bugs and breaking things everyday.