feat(api): init hono with rest and trpc

This commit is contained in:
2025-11-09 01:53:24 +02:00
parent 2b5482e9f5
commit d72f3871a4
11 changed files with 311 additions and 2 deletions
+28
View File
@@ -0,0 +1,28 @@
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# deps
node_modules/
# env
.env
.env.production
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store
+1
View File
@@ -0,0 +1 @@
# Basango API
+31
View File
@@ -0,0 +1,31 @@
{
"dependencies": {
"@basango/db": "workspace:*",
"@basango/logger": "workspace:*",
"@hono/node-server": "^1.19.6",
"@hono/zod-openapi": "^1.1.4",
"@scalar/hono-api-reference": "^0.9.24",
"@trpc/server": "^11.7.1",
"ai": "^5.0.89",
"camelcase-keys": "^10.0.1",
"date-fns": "^4.1.0",
"hono": "^4.10.4",
"hono-rate-limiter": "^0.4.2",
"jose": "^6.1.0",
"zod": "^4.1.12",
"zod-openapi": "^5.4.3"
},
"devDependencies": {
"@types/node": "^20.11.17",
"tsx": "^4.7.1",
"typescript": "^5.8.3"
},
"name": "@basango/api",
"private": true,
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js"
},
"type": "module"
}
+85
View File
@@ -0,0 +1,85 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { Scalar } from "@scalar/hono-api-reference";
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";
import { checkHealth } from "@/utils/health";
const app = new OpenAPIHono();
app.use(secureHeaders());
app.use(
"*",
cors({
allowHeaders: [
"Authorization",
"Content-Type",
"accept-language",
"x-trpc-source",
"x-user-locale",
"x-user-timezone",
"x-user-country",
],
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
exposeHeaders: ["Content-Length"],
maxAge: 86400,
origin: process.env.BASANGO_API_ALLOWED_ORIGINS?.split(",") ?? [],
}),
);
app.get("/health", async (c) => {
try {
await checkHealth();
return c.json({ status: "ok" }, 200);
} catch (error) {
return c.json(
{
message: error instanceof Error ? error.message : "Unknown error",
status: "error",
},
500,
);
}
});
app.doc("/openapi", {
info: {
contact: {
email: "engineer@basango.io",
name: "Basango",
url: "https://basango.io",
},
description: "Basango is a platform that leverages AI to revolutionize news curation.",
license: {
name: "AGPL-3.0 license",
url: "https://github.com/midday-ai/midday/blob/main/LICENSE",
},
title: "Basango API",
version: "0.0.1",
},
openapi: "3.1.0",
security: [
{
oauth2: [],
},
{ token: [] },
],
servers: [
{
description: "Production API",
url: "https://api.basango.io",
},
],
});
// Register security scheme
app.openAPIRegistry.registerComponent("securitySchemes", "token", {
description: "Default authentication mechanism",
scheme: "bearer",
type: "http",
"x-speakeasy-example": "BASANGO_API_KEY",
});
app.get("/", Scalar({ pageTitle: "Basango API", theme: "saturn", url: "/openapi" }));
+17
View File
@@ -0,0 +1,17 @@
import type { HonoRequest } from "hono";
export function getGeoContext(req: HonoRequest) {
const headers = req.header();
const country = headers["x-user-country"]?.toUpperCase() ?? null;
const locale = headers["x-user-locale"] ?? null;
const timezone = headers["x-user-timezone"] ?? null;
const ip = headers["x-forwarded-for"] ?? null;
return {
country,
ip,
locale,
timezone,
};
}
+5
View File
@@ -0,0 +1,5 @@
import { checkHealth as checkDbHealth } from "@basango/db/utils/health";
export async function checkHealth(): Promise<void> {
await checkDbHealth();
}
+4
View File
@@ -0,0 +1,4 @@
export function parseInputValue(value?: string | null) {
if (value === null) return null;
return value ? JSON.parse(value) : undefined;
}
+16
View File
@@ -0,0 +1,16 @@
export function buildSearchQuery(input: string) {
const trimmed = input.trim();
if (!trimmed) {
return "";
}
return trimmed
.split(/\s+/)
.map((term) => {
// Escape special characters for PostgreSQL full-text search
// Special characters: & | ! ( ) : * ' " + - ~
const escaped = term.toLowerCase().replace(/[&|!():*'"+~-]/g, "\\$&");
return `${escaped}:*`;
})
.join(" & ");
}
+21
View File
@@ -0,0 +1,21 @@
import { logger } from "@basango/logger";
import { z } from "zod";
export function validateResponse(data: unknown, schema: z.ZodSchema) {
const result = schema.safeParse(data);
if (!result.success) {
const cause = z.treeifyError(result.error);
logger.error(cause);
return {
data: null,
details: cause,
error: "Response validation failed",
success: false,
};
}
return result.data;
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"extends": "@basango/tsconfig/base.json",
"include": ["src"],
"references": []
}