Service pattern with Hono.js

Hey Im coming from Expressjs backend dev background, and its a bit confusing to me how to implement service pattern with Hono js on top of Cloudflare workers. What i want to achieve is create a singleton object of thirdparty SDK service in /services/engine.ts file.
const engine = new Engine(env.ENGINE_API_KEY)
const engine = new Engine(env.ENGINE_API_KEY)
And then inside hono routes use it like this:
const app = new Hono()
app.get(async c => {
const res = await engine.get("foo")
return c.json(res)
})
const app = new Hono()
app.get(async c => {
const res = await engine.get("foo")
return c.json(res)
})
How can I achieve this? Im getting error inside engine.ts file, since it cannot access env vars for some reason.
16 Replies
IEatBeans
IEatBeans2mo ago
I’m not completely understanding, but since this is serverless you cannot have any objects that persist and are shared across workers. You will need to create that engine object each time it’s used. You get the env variable from the request context
flow
flow2mo ago
oh shoot. so for each route, i need to create it again and again? i have tons of routes...
IEatBeans
IEatBeans2mo ago
I think so. That’s how serverless works. It’s meant for stateless endpoints. What does creating that engine do? Does it need to persist data across multiple calls, or does it make a connection to something?
flow
flow2mo ago
yeah, makes connection to a 3rd party service. creates a client. that i need to use inside routes
IEatBeans
IEatBeans2mo ago
Yea, you would need to do that each time your worker handles an api call Is that an expensive process or can it be done many times like this? I haven’t used durable objects yet and I’m trying to learn about them. They may be able to help you here, since I think they can let you keep objects in memory or something. Definitely worth taking a look at the docs, but I really do not understand them so 🤷🏼‍♂️
flow
flow2mo ago
@IEatBeans thanks not expensive, just too much boilerplate, considering i have 100s of routes @rickyrobinett
IEatBeans
IEatBeans2mo ago
You could put all the boilerplate and config for the engine in a function, and then just call it in each worker? The code will still run and create the client each time, but you won’t have to see that every time
flow
flow2mo ago
yeah, thats what im doing right now, as i couldnt find any better way
IEatBeans
IEatBeans2mo ago
It’s hard to migrate from a stateful backend like node or express to serverless, because you really have to think about it differently. It’s much easier to start a project completely serverless (if you can)
rickyrobinett
rickyrobinett2mo ago
@yusukebe who will have the best insight on doing this with Hono
Hello, I’m Allie!
Something like this?
import { Hono } from "hono";

// Fake SDK for reference
class SomeSDK {
constructor(str: string) {

}
}
const app = new Hono<{ Variables: { SDK: SomeSDK }}>();

app.use((ctx, next) => {
ctx.set("SDK", new SomeSDK("test"));
return next();
});

app.get("/", (ctx) => {
const SDKInstance = ctx.get("SDK");
// Do something with the SDK
return new Response();
});
import { Hono } from "hono";

// Fake SDK for reference
class SomeSDK {
constructor(str: string) {

}
}
const app = new Hono<{ Variables: { SDK: SomeSDK }}>();

app.use((ctx, next) => {
ctx.set("SDK", new SomeSDK("test"));
return next();
});

app.get("/", (ctx) => {
const SDKInstance = ctx.get("SDK");
// Do something with the SDK
return new Response();
});
Don't mind the fake SDK there
yusukebe
yusukebe2mo ago
Hi @flow Indeed, you have to create the engine instance per route in the Cloudflare environment. Then, you can use a simple factory method to do it like the following:
import { Hono } from 'hono'

type Env = {
Bindings: {
TOKEN: string
}
Variables: {
client: Client
}
}

export const createAppWithDB = () => {
const app = new Hono<Env>()
app.use((c) => {
const client = initClient(c.env.TOKEN)
c.set('client', client)
})
return app
}
import { Hono } from 'hono'

type Env = {
Bindings: {
TOKEN: string
}
Variables: {
client: Client
}
}

export const createAppWithDB = () => {
const app = new Hono<Env>()
app.use((c) => {
const client = initClient(c.env.TOKEN)
c.set('client', client)
})
return app
}
Then you can use it in your routes:
const app = createAppWithDB()

app.post('/api', (c) => {
c.var.client.put(/* */)
// ...
})
const app = createAppWithDB()

app.post('/api', (c) => {
c.var.client.put(/* */)
// ...
})
Hello, I’m Allie!
Jinx! Your example isn't per-route though, right? You create the appWithDB once, then it works in every route, no? At least for that Worker anyway(and I guess as long as you aren't mixing multiple routers together/using other handlers).
yusukebe
yusukebe2mo ago
As a syntax, one app would cover every route as you imagine. But, actually, the app will be created per request.
Hello, I’m Allie!
Oh, like that Wait If you aren't calling the new Hono() constructor within the fetch() handler then wouldn't it be able to be reused over multiple requests, asuming they hit the same isolate? It wouldn't reuse the DB client instance, but it would reuse the app itself
yusukebe
yusukebe2mo ago
Sorry for confusing! new Hono() is called once outside the fetch() as you mentioned. But the middleware that is defined in the factory is called everytime:
app.use((c) => {
const client = initClient(c.env.TOKEN)
c.set('client', client)
})
app.use((c) => {
const client = initClient(c.env.TOKEN)
c.set('client', client)
})
So you are right!