DEV Community

Muhammad Ridho
Muhammad Ridho

Posted on

Bun, Hono, Vite, TailwindCSS: An amazing combination has just been revealed!

Bun is great, yes!, Lately everyone is amazed and I was able to play around with it today, and I found it is pretty awesome to use it alongside with Hono, Vite and TailwindCSS.

Without further ado, here I show you the things I have done.

First, create a project with bun, its way way faster than usual.

bun create hono my-hono-app
cd my-hono-app
bun install
Enter fullscreen mode Exit fullscreen mode

For demo purpose, we'll use some popular libs e.g. jose.

bun add jose
Enter fullscreen mode Exit fullscreen mode

Change the tsconfig.json slightly to be like so:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "types": [
      "bun-types"
    ],
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's put some code making use of jose, e.g. create a jwt by ourself, and we place it in src/core.ts:

import * as jose from "jose";

const secret = "πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯";
const algo = "HS512";

const secretBytes = new TextEncoder().encode(secret);
export const secretB64 = btoa(
  Array.from(secretBytes, (x) => String.fromCodePoint(x)).join("")
);

export const createJWT = async () => {
  return await new jose.SignJWT({
    claims: {
      userId: "ridho",
    },
  })
    .setProtectedHeader({ alg: algo })
    .setAudience("urn:audience")
    .setIssuer("urn:issuer")
    .setSubject("urn:subject")
    .setIssuedAt()
    .setExpirationTime("5m")
    .sign(secretBytes);
};
Enter fullscreen mode Exit fullscreen mode

Take note from the above code on how we should create base64 string based on this MDN guide. Later we can validate manually if our jwt and the secretBytes is correct by pasting the token and secret to jwt.io.

Now lets modify the server entry file in src/index.ts.

import { Hono } from "hono";
import { createJWT, secretB64 } from "./core";

const app = new Hono();

app.get("/", async (c) =>
  c.text(`Secret: ${secretB64} JWT: ${await createJWT()}`)
);

export default app;
Enter fullscreen mode Exit fullscreen mode

Yes, its ready. We can now start the development server:

bun run --hot src/index.ts
Enter fullscreen mode Exit fullscreen mode

Great, thats it, very simple and easy! With just very little effort and a few files below, we can serve about 100k request per seconds.

tree -I node_modules 
.
β”œβ”€β”€ bun.lockb
β”œβ”€β”€ package.json
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ core.ts
β”‚Β Β  └── index.ts
└── tsconfig.json

2 directories, 6 files
Enter fullscreen mode Exit fullscreen mode

Ok, lets go further. Instead of simple text response, we want to serve html. Here is how:

import { html } from "hono/html";

app.get("/hello", (c) =>
  c.html(html`<!DOCTYPE html>
    <html>
      <head>
        <title>Simple demo</title>
      </head>
      <body>
        <h1>Ok</h1>
      </body>
    </html>`)
);
Enter fullscreen mode Exit fullscreen mode

I want to write content with jsx you say. Ok, its easy too. First create our jsx file hello.tsx as below:

export function Hello() {
  return (
    <div>
      <h1>Hello there!</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

And use it just like usual, in this simple case, we'll put the jsx into our previous html body:

import { html } from "hono/html";
import { jsx } from "hono/jsx";
import { Hello } from "./hello";

app.get("/hello", (c) => {
  const content = jsx(Hello, {});  // <-- HERE IS our JSX content
  return c.html(html`<!DOCTYPE html>
    <html>
      <head>
        <title>Simple demo</title>
      </head>
      <body>
        ${content}
      </body>
    </html>`);
});
Enter fullscreen mode Exit fullscreen mode

Yeah, that is awesome, and there is more! We can have the great and famous Vite and all it stuffs, e.g. tailwindcss to works too.

There is @hono/vite-dev-server that is being actively developed by the author and for sure it will become great when its ready.

During this time, I've explored how to manually integrate Vite with all of our stuff above so far. It will be just brief steps, i believe you could follow along.

Add the necessary deps:

bun add --dev vite tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

Init tailwindcss config:

bunx tailwindcss init --esm --postcss
Enter fullscreen mode Exit fullscreen mode

Create vite.config.ts:

import { defineConfig } from "vite";

export default defineConfig({
  build: {
    manifest: true,
    rollupOptions: {
      input: "/src/client.tsx",
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

In src/client.tsx is where our client code live and managed by vite. For now, it simply contains import to tailwind css entry file.

tailwind.config.js:

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./src/**/*.tsx"],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

postcss.config.js:

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
Enter fullscreen mode Exit fullscreen mode

Add type=module to package.json:

{
  "type": "module",
  "scripts": {
    "dev": "bun run --hot src/index.ts",
    "vite-dev": "vite",
    "build": "vite build",
    "serve": "NODE_ENV=production bun run src/index.ts"
  },
  "dependencies": {
    "hono": "^3.5.4",
    "jose": "latest"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.15",
    "bun-types": "^0.6.2",
    "postcss": "^8.4.29",
    "tailwindcss": "^3.3.3",
    "vite": "^4.4.9"
  }
}
Enter fullscreen mode Exit fullscreen mode

Add or modify target, module and moduleResolution to tsconfig.json:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "strict": true,
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "types": [
      "bun-types"
    ],
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
  }
}
Enter fullscreen mode Exit fullscreen mode

src/style.css:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

src/client.tsx:

import "./style.css"
Enter fullscreen mode Exit fullscreen mode

src/hello.tsx:

export function Hello() {
  return (
    <div class="text-2xl text-red-600 grid h-[100dvh] items-center justify-center text-center">
      <div>
        <h1 class="text-4xl font-bold">Hello there!</h1>
        <p class="font-mono">
          Bun, Hono, Vite, TailwindCSS <br />
          πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯
        </p>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/index.ts:

import { Hono } from "hono";
import { serveStatic } from "hono/bun";
import { html, raw } from "hono/html";
import { jsx } from "hono/jsx";

import { createJWT, secretB64 } from "./core";
import { Hello } from "./hello";

const app = new Hono();

app.use("/assets/*", serveStatic({ root: "./dist" }));

app.get("/", async (c) =>
  c.text(`Secret: ${secretB64} JWT: ${await createJWT()}`)
);

const isProd = process.env.NODE_ENV === "production";

const manifestPath = "../dist/manifest.json";
const cssFile = isProd
  ? (await import(manifestPath)).default["src/client.tsx"]?.css?.at(0)
  : null;

app.get("/hello", (c) => {
  const content = jsx(Hello, {});
  return c.html(html`<!DOCTYPE html>
    <html>
      <head>
        <title>Simple demo</title>
        ${cssFile ? raw(`<link rel="stylesheet" href="${cssFile}">`) : null}
      </head>
      <body class="bg-green-200">
        ${content}
        ${isProd
          ? null
          : raw(`<script
                type="module"
                src="http://localhost:5173/@vite/client"
            ></script>
            <script
                type="module"
                src="http://localhost:5173/src/client.tsx"
            ></script>`)}
      </body>
    </html>`);
});

export default app;
Enter fullscreen mode Exit fullscreen mode

To start development, start bun and vite on separate terminal:

bun run vite-dev
Enter fullscreen mode Exit fullscreen mode
bun run dev
Enter fullscreen mode Exit fullscreen mode

For production, do build and run:

bun run build
bun run serve
Enter fullscreen mode Exit fullscreen mode

Thats it.

Thank you for reading, have a good weekend!

Top comments (0)