Simple API gateway for webhooks

feat: cache data whenever possible

finxol.io 080be79a 641b4660

verified
Changed files
+99 -12
src
+10
deno.lock
···
{
"version": "5",
"specifiers": {
"npm:@hono/ua-blocker@~0.1.9": "0.1.9_hono@4.9.6",
"npm:@types/node@*": "24.2.0",
"npm:hono@^4.9.6": "4.9.6",
"npm:zod@^4.1.5": "4.1.5"
},
"npm": {
"@hono/ua-blocker@0.1.9_hono@4.9.6": {
···
{
"version": "5",
"specifiers": {
+
"jsr:@std/crypto@*": "1.0.5",
+
"jsr:@std/encoding@*": "1.0.10",
"npm:@hono/ua-blocker@~0.1.9": "0.1.9_hono@4.9.6",
"npm:@types/node@*": "24.2.0",
"npm:hono@^4.9.6": "4.9.6",
"npm:zod@^4.1.5": "4.1.5"
+
},
+
"jsr": {
+
"@std/crypto@1.0.5": {
+
"integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40"
+
},
+
"@std/encoding@1.0.10": {
+
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
+
}
},
"npm": {
"@hono/ua-blocker@0.1.9_hono@4.9.6": {
+1 -1
src/main.ts
···
import { aiBots, useAiRobotsTxt } from "@hono/ua-blocker/ai-bots"
import { config } from "../config.ts"
import { sensors } from "./sensors.ts"
const app = new Hono()
.route("/sensors", sensors)
···
})
if (Deno.env.get("DENO_ENV") === "dev") {
-
const kv = await Deno.openKv()
const { default: data } = await import("../test.json", { with: { type: "json" } })
kv.set(
["sensors", "latest"],
···
import { aiBots, useAiRobotsTxt } from "@hono/ua-blocker/ai-bots"
import { config } from "../config.ts"
import { sensors } from "./sensors.ts"
+
import { kv } from "./util.ts"
const app = new Hono()
.route("/sensors", sensors)
···
})
if (Deno.env.get("DENO_ENV") === "dev") {
const { default: data } = await import("../test.json", { with: { type: "json" } })
kv.set(
["sensors", "latest"],
+26 -11
src/sensors.ts
···
import { Hono } from "hono"
import { validator } from "hono/validator"
import { z } from "zod"
-
import { tryCatch } from "./util.ts"
const SensorsSchema = z.object({
sensorData: z.array(
···
type Sensors = z.infer<typeof SensorsSchema>
-
const kv = await Deno.openKv()
const sensors = new Hono()
.get("/country", async (c) => {
const data = await kv.get<Sensors>(["sensors", "latest"])
if (!data.value) {
return c.text("No data found", 404)
}
const location = data.value.sensorData.find((sensor) =>
sensor.sensorType === "location"
)?.data
···
return c.text("No location data found", 404)
}
-
const geocode = await tryCatch(
-
fetch(
-
`https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${
-
Deno.env.get("GEOAPIFY_API_KEY")
-
}`,
-
)
-
.then((res) => res.json()),
)
if (!geocode.success) {
···
return c.text("Invalid country data", 400)
}
-
return c.json({
country: country.data.features[0].properties.country,
country_code: country.data.features[0].properties.country_code,
-
})
})
.get("/get", async (c) => {
const data = await kv.get<Sensors>(["sensors", "latest"])
···
import { Hono } from "hono"
import { validator } from "hono/validator"
import { z } from "zod"
+
import { fetchWithCache, kv } from "./util.ts"
const SensorsSchema = z.object({
sensorData: z.array(
···
type Sensors = z.infer<typeof SensorsSchema>
+
type Country = {
+
country: string
+
country_code: string
+
}
const sensors = new Hono()
.get("/country", async (c) => {
+
// Served cached data if available
+
const cached = await kv.get<Country>(["country"])
+
if (cached.value) {
+
console.info("Serving cached country data")
+
return c.json(cached.value)
+
}
+
+
// Fetch sensors data from KV
const data = await kv.get<Sensors>(["sensors", "latest"])
if (!data.value) {
return c.text("No data found", 404)
}
+
// Extract location from sensors data
const location = data.value.sensorData.find((sensor) =>
sensor.sensorType === "location"
)?.data
···
return c.text("No location data found", 404)
}
+
// Extract geocode from location data
+
const geocode = await fetchWithCache<Country>(
+
`https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${
+
Deno.env.get("GEOAPIFY_API_KEY")
+
}`,
)
if (!geocode.success) {
···
return c.text("Invalid country data", 400)
}
+
const ret = {
country: country.data.features[0].properties.country,
country_code: country.data.features[0].properties.country_code,
+
}
+
+
// Cache country data to KV store (expiry set for 2 hours)
+
await kv.set(["country"], ret satisfies Country, { expireIn: 60 * 60 * 2 })
+
+
return c.json(ret)
})
.get("/get", async (c) => {
const data = await kv.get<Sensors>(["sensors", "latest"])
+62
src/util.ts
···
/**
* Wraps a promise in a try/catch block and returns a Result object representing
* either a successful value or an error.
···
return { success: false, value: null, error: error as E }
}
}
···
+
import { crypto } from "jsr:@std/crypto"
+
import { encodeHex } from "jsr:@std/encoding/hex"
+
+
export const kv = await Deno.openKv()
+
/**
* Wraps a promise in a try/catch block and returns a Result object representing
* either a successful value or an error.
···
return { success: false, value: null, error: error as E }
}
}
+
+
/**
+
* Fetches data from a URL with caching.
+
* Cache is stored in the KV
+
* @param url - The URL to fetch data from.
+
* @returns A promise that resolves to an object with success status, value, and error.
+
*/
+
export async function fetchWithCache<T>(url: string): Promise<
+
| {
+
success: true
+
value: T
+
error: null
+
}
+
| {
+
success: false
+
value: null
+
error: string
+
}
+
> {
+
// Calculate the hash of the request URL
+
const keybuffer = new TextEncoder().encode(url)
+
const hashBuffer = await crypto.subtle.digest("SHA-256", keybuffer)
+
const hash = encodeHex(hashBuffer)
+
+
// Check if the data is already in cache
+
const cached = await kv.get<T>(["fetch-cache", hash])
+
if (cached.value) {
+
console.info("Serving cached request data")
+
return {
+
success: true,
+
value: cached.value,
+
error: null,
+
}
+
}
+
+
// Fetch the data from the URL
+
const data = await tryCatch(
+
fetch(url).then((res) => res.json() as T),
+
)
+
+
if (!data.success) {
+
return {
+
success: false,
+
value: null,
+
error: data.error.message,
+
}
+
}
+
+
// Store the fetched data in cache, with a 1-day expiration
+
await kv.set(["fetch-cache", hash], data.value, { expireIn: 60 * 60 * 24 })
+
+
return {
+
success: true,
+
value: data.value,
+
error: null,
+
}
+
}