TurboJet
API

Mendefinisikan endpoint API

Schema Zod, service, router Hono, mounting, dan path OpenAPI opsional.

Walkthrough ini memakai modul feedback sebagai contoh konkret. Langkah yang sama berlaku untuk domain apa pun di packages/api/src/modules/.

Untuk vertical slice produk lengkap (UI + mutation + i18n), lihat Membangun fitur.

1. Tambahkan schema Zod

Letakkan schema input di packages/validations/src/{domain}/ dan ekspor dari paket:

packages/validations/src/feedback/feedback.ts
import { z } from "zod";

export const feedbackTypeSchema = z.enum(["bug", "feature", "general"]);

export const createFeedbackSchema = z.object({
	message: z
		.string({ message: "Please enter your feedback" })
		.trim()
		.min(10, "Feedback must be at least 10 characters")
		.max(1000, "Feedback must be at most 1000 characters"),
	type: feedbackTypeSchema,
	email: z
		.union([z.email({ error: "Invalid email address" }), z.literal("")])
		.optional(),
});

export type CreateFeedbackData = z.infer<typeof createFeedbackSchema>;

2. Implementasikan service

Simpan logika bisnis dan akses database di service — bukan di router:

packages/api/src/modules/feedback/service.ts
import { db } from "@repo/database";
import type { CreateFeedbackData } from "@repo/validations/feedback";
import type { SafeSession } from "@repo/validations/user";
import { fail } from "../../lib/http";

export async function createFeedback({
	data,
	session,
	ipAddress,
}: {
	data: CreateFeedbackData;
	session: SafeSession | null;
	ipAddress?: string | null;
}) {
	const email = session?.user.email ?? (data.email || null);

	if (!session && !email) {
		fail(400, "Email is required when you are not signed in");
	}

	const feedback = await db.feedback.create({
		data: {
			message: data.message,
			type: data.type,
			email,
			userId: session?.user.id ?? null,
			ipAddress: ipAddress ?? null,
		},
		select: { id: true, type: true, createdAt: true },
	});

	return {
		success: true as const,
		feedback: {
			id: feedback.id,
			type: feedback.type,
			createdAt: feedback.createdAt.toISOString(),
		},
	};
}

Gunakan fail(status, message) dari packages/api/src/lib/http.ts untuk error HTTP bertipe.

3. Definisikan router Hono

Urutan middleware penting: rate limit → auth (jika ada) → zValidator → handler.

packages/api/src/modules/feedback/router.ts
import { Hono } from "hono";
import { createFeedbackSchema } from "@repo/validations/feedback";
import { zValidator } from "../../lib/validator";
import { rateLimit } from "../../middleware/rate-limit";
import { getSessionFromHeaders } from "../../lib/session";
import { createFeedback } from "./service";

export const feedbackRouter = new Hono().post(
	"/",
	rateLimit("WRITE"),
	zValidator("json", createFeedbackSchema),
	async (c) => {
		const session = await getSessionFromHeaders(c.req.raw.headers);
		const result = await createFeedback({
			data: c.req.valid("json"),
			session,
			ipAddress: c.req.header("x-forwarded-for") ?? null,
		});
		return c.json(result);
	},
);

Handler terautentikasi memakai requireAuth dan membaca c.get("session") — lihat Melindungi endpoint.

Panduan penamaan

Lebih baik method dan segment path Hono yang jelas daripada satu handler raksasa:

IntentBentuk tipikal
ListGET /
CreatePOST /
Find oneGET /:id
UpdatePATCH /:id atau PUT /:id
DeleteDELETE /:id

Ini adalah route Hono biasa, bukan file prosedur oRPC terpisah.

4. Mount router

Daftarkan modul pada app router:

packages/api/src/index.ts
import { feedbackRouter } from "./modules/feedback/router";

export const routes = new Hono()
	.route("/me", meRouter)
	// …
	.route("/feedback", feedbackRouter);

Catch-all marketing memasang app penuh di /api, jadi ini menjadi POST /api/feedback.

5. Opsional: OpenAPI / Scalar

Jika Anda ingin path muncul di Scalar, tambahkan ke dokumen yang dirawat manual di packages/api/src/openapi.ts. OpenAPI tidak di-generate otomatis dari router.

Terkait

Di halaman ini