Introduction to Hono RPC for Next.Js Development
This article concerns with hono RPC and Next.Js for typesafe API routes
Table of contents
Why care about Hono RPC?
Hono, is a fast and lightweight web framework made to run on multiple runtimes. Originally made for cloudfare workers, now also support various if not all web frameworks and runtimes. But the question remains the same, why even need hono RPC? Well the reality of situation is API routes are kinda vague, just like replies from your crush. One can validate the requests and responses but that doesn’t change the fact that we have no type safety for the API routes.
The RPC feature allows sharing of the API specifications between the server and the client.
In short one can make scalable and type safe API routes with all the production ready features
Setting up Hono Rpc for Next.Js
Before we proceed, i would like to provide all the necessary links from the documentation for cross referencing. You can also leverage libraries such as react-query for efficient API calls
Ofc first install hono with your favorite package manager
In
src/app
, make a catch all route[[...route]]/route.ts
, assuming you are using app routerimport { Hono } from 'hono' import { handle } from 'hono/vercel' export const runtime = 'edge' const app = new Hono().basePath('/api') app.get('/hello', (c) => { return c.json({ message: 'Hello Next.js!', }) }) export const GET = handle(app) export const POST = handle(app)
Make a new folder such as features in the source directory
src/features
🚧Make sure to make this folder outside of theapp
folder as otherwise it may cause build errorsHere’s one of the auth feature example
inside features folder, make an auth directory
features/auth
inside auth make an api folder
auth/api
, in this api folder, make a login file, namelyuse-login.ts
import { useMutation, useQueryClient } from "@tanstack/react-query"; // react query import { InferRequestType, InferResponseType } from "hono"; import { toast } from "sonner"; // toast library import { client } from "@/lib/rpc"; import { useRouter } from "next/navigation"; type ResponseType = InferResponseType<(typeof client.api.auth.login)["$post"]>; type RequestType = InferRequestType<(typeof client.api.auth.login)["$post"]>; export function Login() { const router = useRouter(); const queryClient = useQueryClient(); const mutation = useMutation<ResponseType, Error, RequestType>({ mutationFn: async ({ json }) => { const res = await client.api.auth.login.$post({ json }); if (!res.ok) throw new Error("Failed to login"); return await res.json(); }, onSuccess: () => { toast.success("Logged In"); router.refresh(); queryClient.invalidateQueries({ queryKey: ["current"] }); }, onError: () => { toast.error("Error while logging in, plesase try again"); }, }); return mutation; }
inside the same auth folder make another folder namely server with a file
server/route.ts
import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; import { ID } from "node-appwrite"; import { deleteCookie, setCookie } from "hono/cookie"; import { loginSchema, registerSchema } from "@/lib/schemas/auth-schema"; import { createAdminClient } from "@/lib/appwrite"; import { AUTH_COOKIE } from "@/lib/constants"; import { sessionMiddleware } from "@/lib/middlewares/session-middleware"; const app = new Hono() .get("/current", sessionMiddleware, async (c) => { const user = c.get("user"); return c.json({ data: user }); }) .post("/login", zValidator("json", loginSchema), async (c) => { const { email, password } = c.req.valid("json"); const { account } = await createAdminClient(); const session = await account.createEmailPasswordSession(email, password); setCookie(c, AUTH_COOKIE, session.secret, { path: "/", httpOnly: true, secure: true, sameSite: "strict", maxAge: 60 * 60 * 24 * 30, }); return c.json({ message: "success" }); }) .post("/register", zValidator("json", registerSchema), async (c) => { const { account } = await createAdminClient(); const { email, password, name } = c.req.valid("json"); setCookie(c, AUTH_COOKIE, session.secret, { path: "/", httpOnly: true, secure: true, sameSite: "strict", maxAge: 60 * 60 * 24 * 30, }); return c.json({ success: true }); }) .post("/logout", sessionMiddleware, async (c) => { const account = c.get("account"); deleteCookie(c, AUTH_COOKIE); await account.deleteSession("current"); return c.json({ success: true }); }); export default app;
And looks like you are all done, now you just need to make call to the backend
"use client"; import Image from "next/image"; import { Eye, EyeClosed, Github } from "lucide-react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; import { TextDivider } from "@/components/text-divider"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { loginSchema, loginValues } from "@/lib/schemas/auth-schema"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Login } from "@/features/auth/api/use-login"; export function SignInCard() { const { mutate, isPending } = Login(); const [showPassword, setShowPassword] = useState(false); const form = useForm<loginValues>({ defaultValues: { email: "", password: "", }, resolver: zodResolver(loginSchema), }); function onSubmit(values: loginValues) { mutate({ json: values }); } return ( <Card className="h-full w-full border-none shadow-none md:w-[487px]"> <CardHeader className="flex items-center justify-center p-7 text-center"> <CardTitle className="capitalize">Welcome Back</CardTitle> </CardHeader> <div className="px-7"> <Separator /> </div> <CardContent className="p-7"> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input placeholder="your account email" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <div className="flex justify-between gap-2"> <Input type={showPassword ? "text" : "password"} placeholder="your account password" className="" {...field} /> <button type="button" onClick={() => setShowPassword(!showPassword)} > {showPassword ? ( <Eye className="size-5 text-neutral-600" /> ) : ( <EyeClosed className="size-5 text-neutral-600" /> )} </button> </div> </FormControl> <FormMessage /> </FormItem> )} /> <Button disabled={isPending} size="lg" className="w-full" type="submit" > Submit </Button> </form> </Form> </CardContent> <div className="px-7"> <TextDivider text="Other login methods" /> </div> <CardContent className="flex flex-col gap-y-4 p-7"> <Button className="w-full" variant="outline" size="lg" disabled={isPending} > <Image src="/google.svg" height={25} width={25} alt="google" className="mt-0.5" /> Login with google </Button> <Button className="w-full" variant="outline" size="lg" disabled={isPending} > <Github /> Login with github </Button> </CardContent> </Card> ); }
Resources
This video provides great tutorial and extensive setup of the this paradigm