Write a lot more about the process of building this blog, and fix some in-file documentation.
This commit is contained in:
parent
b1221ed0ed
commit
ba14bd6adc
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Bun and Hono Are All You Need
|
title: Adventures with Bun and Hono
|
||||||
description: A blog post wherein I talk about building this blog with Bun and Hono.
|
description: A blog post wherein I talk about building this blog with Bun and Hono.
|
||||||
date: 2024-08-01
|
date: 2024-08-01
|
||||||
tags:
|
tags:
|
||||||
@ -11,22 +11,142 @@ slug: fullstack-hono
|
|||||||
draft: true
|
draft: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Bun and Hono Are You Need
|
# Context and My Blog
|
||||||
|
|
||||||
I wanted to write a small journey I have been on over the last couple of months. I have been looking for a reason to actually use Bun for a project and I think I found it: to rebuild my blog.
|
I like my blog. I don't write nearly as much as I should for it but at the endof the day I like the three small posts that I have put into it. However, I never really liked how I had it set up. I have no real qualms with the SSG I chose ([11ty](https://www.11ty.dev/)) or the web server to host the HTML (NGiNX) but I never really felt like it was _mine_.
|
||||||
|
|
||||||
## Some Context
|
I have to be very clear: 11ty is very nice, very good, and has a fantasic community and plugins. This post, and this work, was not in response to anything specific to 11ty. Go use it if you liked it.
|
||||||
|
|
||||||
My old blog was built with 11ty and I deployed it manually with `rsync` behind a very simple NGiNX virtualhost. This was fine but I found I never wanted to actually write for it after a while. It didn't feel like _mine_.
|
Over the last few months I have been noodling with [Bun](https://bun.sh/) and [Hono](https://hono.dev/) as a stack to build web applications with. While I know JavaScript isn't all that popular insome of the circles I travel in - its a language that, oddly enough, gives me joy. And a runtime like Deno or Bun giving us TypeScript out of the box is a huge win.
|
||||||
|
|
||||||
## Bun
|
Because of this, I was looking at building a simple blog application with a micro-framework, similar to Express.js. I thought about using Express.js itself but the fact that its old _[sic]_ and slow has made look for other options. One particular thing I needed the framework to do - is have a view layer of some sort. I ended up picking Hono as it ticked all the boxes for me and I can say, after building very simple applications with it, it brings me joy. One of the big selling points of Hono was the [`jsxRenderer` middleware](https://hono.dev/docs/middleware/builtin/jsx-renderer) that allows me to write plain old JSX/TSX and create frontend templates and components.
|
||||||
|
|
||||||
I work with a lot of JavaScript in my day-to-day job. Over the time we moved to TypeScript and all the things that come with that. Last year, I got really into the state of Bun and Deno.
|
Finally, I decided that my entire application will just run in memory. While I have static Markdown files that make up my posts, on each build of the application it will parse the files, organize the post data and render them to the client in the response. I have thought about maybe parsing the Markdown into a relational database like SQLite and using the built-in Bun functionality to handle that database.
|
||||||
|
|
||||||
## Hono
|
But that made no sense once I _actually_ thought it. What advantage does having an in-memory SQLite instance of my blog give me over just rendering a bunch HTML and holding _that_ in memory? These are good conversations I have had with myself that I wish I had with someone else to get to the conclusion faster.
|
||||||
|
|
||||||
I was looking at Bun-specific Express.js alternatives and a colleague of mine pointed me to Hono. I looked through the Hono documentation and it just clicked with me. Most of these micro-frameworks do a lot of the same thing.
|
_Note:_ I am going to talk a lot about JavaScript and TypeScript, and I want to make it very clear that I am talking about running JavaScript on the server. I feel like when people talk about JavaScript, there is an assumption we are talking about building frontend SPAs and not high-IO server applications (for good reasons, honestly). However, in my career and experience, the bulk of JavaScript I have written runs on Node.js. Anyways, after such a long introduction, here is my blog post about what I learned building a very basic blog with Hono and Bun.
|
||||||
|
|
||||||
## JSX
|
# Organizing Hono applications
|
||||||
|
|
||||||
## Pulling it All Together
|
I make no effort to hide the fact that I come from the MVC Web Framework world. I have spent a lot of time in my career thinking of web applications in the terms of Models, Views, and Controllers. This has made a lot of sense to me over the years, and I posit that it's still a great framework for organizing your code. However when it comes to JavaScript, it feels verbose to create classes full of methods to handle requests and responses. I think this verbosity comes from how JavaScript code is actually organized for Node.js (Deno, Bun, etc).
|
||||||
|
|
||||||
|
JavaScript can best be described as _Modules_. These modules are single files that define some form of behaviour, state, or shape to data. Because of this, I find its not particularly useful to oganize code into Classes unless there is a specific state that many methods/functions need to keep track of. Because of this revelation in how JavaScript code is run and organized, I feel nothing but **regret** for creating many, many Controller classes from scratch for Express.js applications.
|
||||||
|
|
||||||
|
So with my understanding that a Class in JavaScript should be a collection of methods with _shared state_ - why should we make a Controller class with discrete methods for handling requests and responses when each individual method has its own state? Previously, I would assume that we would want to share some sort of resource - like a Repository class for interacting with a database. But the more I read and tinkered with the Hono framework (_and this is not a Hono specific thought_) the more I realized we should be centering our dependencies within the `Context` object of the Hono application.
|
||||||
|
|
||||||
|
I like to call this pattern Handler, Service, Presentation. This pattern is nearly identical to MVC and you can immediately see the analogues to the original acronym. I don't think there is a clear advantage of using these words in particular, other than it can hopefully erode some of the _web-brainrot_ on how we organize our we applications.
|
||||||
|
|
||||||
|
## An Example of a Service
|
||||||
|
|
||||||
|
Let's think of a Blog. This blog. What services do we have in the code for this blog? Right now, it is solely the `PostService`. This is a class that is given a list of `Post` types, and creates an internal Map of that with the `slug` as a key. From there we can do things like, get all the posts, get the latest post, get a post by slugs, get un-published posts, etc.
|
||||||
|
|
||||||
|
Within the `PostService` module is two helper functions. One of these is an `async` function for reading and parsing the Post markdown files, and the other will utilize that function to instantiate a `PostService` class. This is a great way to do some `async` shit for construction an object (like reading a file from disk).
|
||||||
|
|
||||||
|
Another good way to think of a service, is a Repository class. Think of a class that should handle querying data to and from a database. If you need another example, image an HTTP Client for a specific HTTP API. Think of something that provides data to something else. I guess that's how I'd describe it.
|
||||||
|
|
||||||
|
## What Handlers Are
|
||||||
|
|
||||||
|
Handlers should be thought of as callback functions for particular requests and responses. The _handle_ the request and response. In the world of Hono, we get to decide what Middleware is type'd into the Application and can be accessed within a handler. This allows us to bootstrap our middleware elsewhere and be assured it will be there when it runs.
|
||||||
|
|
||||||
|
Here is the handler for showing a single Post on the blog:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Hono, Context } from 'hono';
|
||||||
|
import { PostPage } from '@blog/templates/Pages/PostPage';
|
||||||
|
import { FourOhFour } from '@blog/templates/Pages/FourOhFour';
|
||||||
|
import { SiteMeta } from '@blog/models/SiteMeta';
|
||||||
|
import { PostService } from '@blog/services/post-file';
|
||||||
|
|
||||||
|
type Posts = {
|
||||||
|
postService: PostService
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = new Hono<{ Variables: Posts }>();
|
||||||
|
|
||||||
|
export async function handleSinglePost(c: Context) {
|
||||||
|
const postSlug: string = c.req.param("slug");
|
||||||
|
const postService: PostService = c.get('postService');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const post = postService.getPost(postSlug);
|
||||||
|
const meta: SiteMeta = {
|
||||||
|
description: post.meta.description,
|
||||||
|
tags: post.meta.tags,
|
||||||
|
author: "Dave Smith-Hayes"
|
||||||
|
};
|
||||||
|
|
||||||
|
return c.render(<PostPage post={post} />, { meta });
|
||||||
|
} catch (e) {
|
||||||
|
const description: string = "Page does not exist.";
|
||||||
|
console.error(description);
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
c.status(404);
|
||||||
|
const meta: SiteMeta = { description };
|
||||||
|
return c.render(<FourOhFour />, { meta });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
posts.get('/:slug', handleSinglePost);
|
||||||
|
export default posts;
|
||||||
|
```
|
||||||
|
_`src/handlers/posts.tsx`_
|
||||||
|
|
||||||
|
Now I haven't done this yet - but if I need to test the `handleSinglePost` function, I can properly mock the `Context` object with the right `PostService` class.
|
||||||
|
|
||||||
|
## How Presentation Works
|
||||||
|
|
||||||
|
Like I mentioned earlier, one of the selling points of using Hono for the framework was its suppose of rendering JSX/TSX with `jsxRenderer` middleware. Its trivial to set up, but you have to remember to re-save your files as `tsx` and `jsx` if you want to use it. I have not spent a lot of time writing JSX/TSX in my life but once I got some of the basics it became super easy to understand.
|
||||||
|
|
||||||
|
One of the first things to realize about JSX/TSX is that its solely a syntax. While React utilizes this it is _not_ React. You could easily compre JSX/TSX to something like Twig, Pug, or another Templating language. The big difference is that its JavaScript centric, and you can really compose the components you build with functions.
|
||||||
|
|
||||||
|
You can set up a super basic Page tempalte that ever page will render within.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Style } from 'hono/css';
|
||||||
|
import { SiteMeta } from '@blog/models/SiteMeta';
|
||||||
|
import { MetaTags } from '@blog/templates/components/MetaTags';
|
||||||
|
|
||||||
|
export function Page({ children, meta }: { children: any, meta: SiteMeta }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>davesmithhayes.com</title>
|
||||||
|
<MetaTags meta={meta} />
|
||||||
|
<Style />
|
||||||
|
<link rel="stylesheet" href="/static/main.css" />
|
||||||
|
<link rel="stylesheet" href="/static/intellij-light.min.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<a href="/">davesmithhayes.com</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<div class="copyright">© 2024 Dave Smith-Hayes</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
_`src/templates/Page`_
|
||||||
|
|
||||||
|
And then you can set up the `jsxRenderer` middleware within the **main** Hono App instatiation.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Page } from "@blog/templates/Page";
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
'*',
|
||||||
|
jsxRenderer(
|
||||||
|
({ children, meta }) => <Page meta={meta}>{children}</Page>,
|
||||||
|
{ docType: true }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono, Context } from 'hono';
|
||||||
import { PostPage } from '@blog/templates/Pages/PostPage';
|
import { PostPage } from '@blog/templates/Pages/PostPage';
|
||||||
import { FourOhFour } from '@blog/templates/Pages/FourOhFour';
|
import { FourOhFour } from '@blog/templates/Pages/FourOhFour';
|
||||||
import { SiteMeta } from '@blog/models/SiteMeta';
|
import { SiteMeta } from '@blog/models/SiteMeta';
|
||||||
@ -8,9 +8,9 @@ type Posts = {
|
|||||||
postService: PostService
|
postService: PostService
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new Hono<{ Variables: Posts }>();
|
const posts = new Hono<{ Variables: Posts }>();
|
||||||
|
|
||||||
app.get('/:slug', async (c) => {
|
export async function handleSinglePost(c: Context) {
|
||||||
const postSlug: string = c.req.param("slug");
|
const postSlug: string = c.req.param("slug");
|
||||||
const postService: PostService = c.get('postService');
|
const postService: PostService = c.get('postService');
|
||||||
|
|
||||||
@ -32,7 +32,8 @@ app.get('/:slug', async (c) => {
|
|||||||
const meta: SiteMeta = { description };
|
const meta: SiteMeta = { description };
|
||||||
return c.render(<FourOhFour />, { meta });
|
return c.render(<FourOhFour />, { meta });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default app;
|
posts.get('/:slug', handleSinglePost);
|
||||||
|
export default posts;
|
||||||
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import { css, Style } from 'hono/css';
|
import { Style } from 'hono/css';
|
||||||
import { SiteMeta } from '@blog/models/SiteMeta';
|
import { SiteMeta } from '@blog/models/SiteMeta';
|
||||||
import { MetaTags } from '@blog/templates/components/MetaTags';
|
import { MetaTags } from '@blog/templates/components/MetaTags';
|
||||||
|
|
||||||
const logoClass = css`
|
|
||||||
font-size: 16pt;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function Page({ children, meta }: { children: any, meta: SiteMeta }) {
|
export function Page({ children, meta }: { children: any, meta: SiteMeta }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -18,7 +14,7 @@ export function Page({ children, meta }: { children: any, meta: SiteMeta }) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class={logoClass}>
|
<div>
|
||||||
<a href="/">davesmithhayes.com</a>
|
<a href="/">davesmithhayes.com</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
Loading…
Reference in New Issue
Block a user