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:
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:
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.
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:
| Intent | Bentuk tipikal |
|---|---|
| List | GET / |
| Create | POST / |
| Find one | GET /:id |
| Update | PATCH /:id atau PUT /:id |
| Delete | DELETE /:id |
Ini adalah route Hono biasa, bukan file prosedur oRPC terpisah.
4. Mount router
Daftarkan modul pada app router:
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.