Simple API gateway for webhooks

feat: cache data whenever possible

finxol.io 080be79a 641b4660

verified
+99 -12
+10
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@std/crypto@*": "1.0.5", 5 + "jsr:@std/encoding@*": "1.0.10", 4 6 "npm:@hono/ua-blocker@~0.1.9": "0.1.9_hono@4.9.6", 5 7 "npm:@types/node@*": "24.2.0", 6 8 "npm:hono@^4.9.6": "4.9.6", 7 9 "npm:zod@^4.1.5": "4.1.5" 10 + }, 11 + "jsr": { 12 + "@std/crypto@1.0.5": { 13 + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" 14 + }, 15 + "@std/encoding@1.0.10": { 16 + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" 17 + } 8 18 }, 9 19 "npm": { 10 20 "@hono/ua-blocker@0.1.9_hono@4.9.6": {
+1 -1
src/main.ts
··· 3 3 import { aiBots, useAiRobotsTxt } from "@hono/ua-blocker/ai-bots" 4 4 import { config } from "../config.ts" 5 5 import { sensors } from "./sensors.ts" 6 + import { kv } from "./util.ts" 6 7 7 8 const app = new Hono() 8 9 .route("/sensors", sensors) ··· 53 54 }) 54 55 55 56 if (Deno.env.get("DENO_ENV") === "dev") { 56 - const kv = await Deno.openKv() 57 57 const { default: data } = await import("../test.json", { with: { type: "json" } }) 58 58 kv.set( 59 59 ["sensors", "latest"],
+26 -11
src/sensors.ts
··· 1 1 import { Hono } from "hono" 2 2 import { validator } from "hono/validator" 3 3 import { z } from "zod" 4 - import { tryCatch } from "./util.ts" 4 + import { fetchWithCache, kv } from "./util.ts" 5 5 6 6 const SensorsSchema = z.object({ 7 7 sensorData: z.array( ··· 128 128 129 129 type Sensors = z.infer<typeof SensorsSchema> 130 130 131 - const kv = await Deno.openKv() 131 + type Country = { 132 + country: string 133 + country_code: string 134 + } 132 135 133 136 const sensors = new Hono() 134 137 .get("/country", async (c) => { 138 + // Served cached data if available 139 + const cached = await kv.get<Country>(["country"]) 140 + if (cached.value) { 141 + console.info("Serving cached country data") 142 + return c.json(cached.value) 143 + } 144 + 145 + // Fetch sensors data from KV 135 146 const data = await kv.get<Sensors>(["sensors", "latest"]) 136 147 if (!data.value) { 137 148 return c.text("No data found", 404) 138 149 } 139 150 151 + // Extract location from sensors data 140 152 const location = data.value.sensorData.find((sensor) => 141 153 sensor.sensorType === "location" 142 154 )?.data ··· 145 157 return c.text("No location data found", 404) 146 158 } 147 159 148 - const geocode = await tryCatch( 149 - fetch( 150 - `https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${ 151 - Deno.env.get("GEOAPIFY_API_KEY") 152 - }`, 153 - ) 154 - .then((res) => res.json()), 160 + // Extract geocode from location data 161 + const geocode = await fetchWithCache<Country>( 162 + `https://api.geoapify.com/v1/geocode/reverse?lat=${location.latitude}&lon=${location.longitude}&apiKey=${ 163 + Deno.env.get("GEOAPIFY_API_KEY") 164 + }`, 155 165 ) 156 166 157 167 if (!geocode.success) { ··· 166 176 return c.text("Invalid country data", 400) 167 177 } 168 178 169 - return c.json({ 179 + const ret = { 170 180 country: country.data.features[0].properties.country, 171 181 country_code: country.data.features[0].properties.country_code, 172 - }) 182 + } 183 + 184 + // Cache country data to KV store (expiry set for 2 hours) 185 + await kv.set(["country"], ret satisfies Country, { expireIn: 60 * 60 * 2 }) 186 + 187 + return c.json(ret) 173 188 }) 174 189 .get("/get", async (c) => { 175 190 const data = await kv.get<Sensors>(["sensors", "latest"])
+62
src/util.ts
··· 1 + import { crypto } from "jsr:@std/crypto" 2 + import { encodeHex } from "jsr:@std/encoding/hex" 3 + 4 + export const kv = await Deno.openKv() 5 + 1 6 /** 2 7 * Wraps a promise in a try/catch block and returns a Result object representing 3 8 * either a successful value or an error. ··· 55 60 return { success: false, value: null, error: error as E } 56 61 } 57 62 } 63 + 64 + /** 65 + * Fetches data from a URL with caching. 66 + * Cache is stored in the KV 67 + * @param url - The URL to fetch data from. 68 + * @returns A promise that resolves to an object with success status, value, and error. 69 + */ 70 + export async function fetchWithCache<T>(url: string): Promise< 71 + | { 72 + success: true 73 + value: T 74 + error: null 75 + } 76 + | { 77 + success: false 78 + value: null 79 + error: string 80 + } 81 + > { 82 + // Calculate the hash of the request URL 83 + const keybuffer = new TextEncoder().encode(url) 84 + const hashBuffer = await crypto.subtle.digest("SHA-256", keybuffer) 85 + const hash = encodeHex(hashBuffer) 86 + 87 + // Check if the data is already in cache 88 + const cached = await kv.get<T>(["fetch-cache", hash]) 89 + if (cached.value) { 90 + console.info("Serving cached request data") 91 + return { 92 + success: true, 93 + value: cached.value, 94 + error: null, 95 + } 96 + } 97 + 98 + // Fetch the data from the URL 99 + const data = await tryCatch( 100 + fetch(url).then((res) => res.json() as T), 101 + ) 102 + 103 + if (!data.success) { 104 + return { 105 + success: false, 106 + value: null, 107 + error: data.error.message, 108 + } 109 + } 110 + 111 + // Store the fetched data in cache, with a 1-day expiration 112 + await kv.set(["fetch-cache", hash], data.value, { expireIn: 60 * 60 * 24 }) 113 + 114 + return { 115 + success: true, 116 + value: data.value, 117 + error: null, 118 + } 119 + }