final code
Setup using Rakkasjs which is a full-stack vite react framework
1 pnpm create rakkas-app my-rakkas-app
hattip
what is hattip ? , it's what powers the backend for Rakkasjs, and allows you to write code that'll run in multiple runtimes and environments, like node, bun,deno, and Cloudflare workers ...
1 pnpm install hattip @hattip/response @hattip/cookie http-status-codes devalue
trpc
What is trpc ? , An easy way to write typesafe APIs in typescript, collocated backend, and frontend share types and give you a typesafe RPC API
1 pnpm install @trpc/client @trpc/react-query @trpc/server superjson zod
prisma
This one is optional as tprc doesn't necessarily need it , but we're trying to recreate the t3-stack which in Nextjs in vite + react, you can use whatever javascript orm / data source
What is rakkasjs ? , The closest thing to a Nextjs on Vite.
To get started we'll create the trpc API endpoint which will be a catch-all route with the all verb.
To create API routes in Rakkasjs we put it in the src/routes/api directory, A simple API route would look something like this
1
2 import { json } from "@hattip/response" ;
3 import { StatusCodes } from "http-status-codes" ;
4 import { RequestContext } from "rakkasjs" ;
5
6 export async function get ( ctx : RequestContext ) {
7 try {
8 return json ( { message : "welcome to rakkas root api" } , { status : StatusCodes . ACCEPTED } ) ;
9 } catch ( error ) {
10 return json ( error , { status : StatusCodes . BAD_REQUEST } ) ; }
11 }
12
13 export async function post ( ctx : RequestContext ) {
14 const body = await ctx . request . json ( ) ;
15 try {
16 return json ( { body } , { status : StatusCodes . ACCEPTED } ) ;
17 } catch ( error ) {
18 return json ( error , { status : StatusCodes . BAD_REQUEST } ) ;
19 }
20 }
21
we're going to be doing alot of relative imports so let's setup tsconfig aliases to point @/ to src/
1
2 {
3 "compilerOptions" : {
4 "target" : "es2020" ,
5 "module" : "ESNext" ,
6 "esModuleInterop" : true ,
7 "forceConsistentCasingInFileNames" : true ,
8 "strict" : true ,
9 "skipLibCheck" : true ,
10 "moduleResolution" : "Node" ,
11 "resolveJsonModule" : true ,
12 "jsx" : "react-jsx" ,
13 "baseUrl" : "." ,
14 "types" : [ "vite/client" ] ,
15
16
17 "paths" : {
18 "@/*" : [ "src/*" ]
19 }
20
21
22
23 }
24 }
25
Our trpc catch-all route
1
2 import { fetchRequestHandler } from '@trpc/server/adapters/fetch' ;
3 import { RequestContext } from 'rakkasjs' ;
4 import { createTRPCContext } from '@/server/trpc' ;
5 import { appRouter } from '@/server/routes/root' ;
6
7 export async function all ( ctx : RequestContext ) {
8 return fetchRequestHandler ( {
9 endpoint : '/api/trpc' ,
10 req : ctx . request ,
11 router : appRouter ,
12 createContext : createTRPCContext ,
13 } ) ;
14 }
The only difference between this and the nextjs create-t3-app is that we're using the fetchRequestHandler instead of the nextjs adapter
hattip like most of the other agnostic runtime servers relies on the standard fetch API.
We'll put all the trpc logic in the src/server directory.
first, we'll create the trpc context
1 export function createTRPCContext ( { req , resHeaders } : FetchCreateContextFnOptions ) {
2 const user = { name : req . headers . get ( 'username' ) ?? 'anonymous' } ;
3
4 return { req , resHeaders , user } ;
5 }
6
then we init trpc
1 const t = initTRPC . context < typeof createTRPCContext > ( ) . create ( {
2 transformer : superjson ,
3 errorFormatter ( { shape , error } ) {
4 return {
5 ... shape ,
6 data : {
7 ... shape . data ,
8 zodError :
9 error . cause instanceof ZodError ? error . cause . flatten ( ) : null ,
10 } ,
11 } ;
12 } ,
13 } ) ;
then we export the router and procedure
1 export const createTRPCRouter = t . router ;
2 export const router = t . router
3 export const publicProcedure = t . procedure ;
Then we can now create the trpc router + it's endpoints
1
2 import { createTRPCRouter , publicProcedure } from '../trpc' ;
3 import { helloRouter } from './hello' ;
4
5
6
7
8 export const appRouter = createTRPCRouter ( {
9 hello : helloRouter ,
10 } ) ;
11
12
13 export type AppRouter = typeof appRouter ;
14
1
2 import { createTRPCRouter , publicProcedure } from "../trpc" ;
3 export const helloRouter = createTRPCRouter ( {
4 hey : publicProcedure . query ( ( opts ) => {
5 return ` Hey there ` ;
6 } ) ,
7 wave : publicProcedure . query ( async ( opts ) => {
8 return ` 🙋♀️ ` ;
9 } ) ,
10
11 } )
With thhat done , our endppoints are ready to consume on the frontend
lets add some frontend dependancies
1 pnpm install @tanstack/react-query react-toastify lucide-react @unpic/react tailwind-merge
Then we configure the trpc client in src/utils/trpc.ts
1
2 import type { AppRouter } from '@/server/routes/root' ;
3 import { createTRPCReact } from '@trpc/react-query' ;
4
5
6 export const trpc = createTRPCReact < AppRouter > ( ) ;
7
Then we create a trpc client for the provider
1
2 import { trpc } from "./trpc" ;
3 import { httpBatchLink } from "@trpc/react-query" ;
4 import superjson from "superjson" ;
5
6 const getBaseUrl = ( url ? : string ) => {
7 if ( typeof window !== "undefined" ) return "" ;
8 const urlObj = new URL ( url as string ) ;
9 if ( process . env . VERCEL_URL ) return ` https:// ${ process . env . VERCEL_URL } ` ;
10 return urlObj . origin ;
11 } ;
12
13
14 export const trpcClient = ( url ? : string ) => {
15 return trpc . createClient ( {
16 links : [
17 httpBatchLink ( {
18 url : ` ${ getBaseUrl ( url ) } /api/trpc ` ,
19 } ) ,
20 ] ,
21 transformer : superjson
22 } ) ;
23 }
24
Then we setup the stack react query provider
1
2 const [ queryClient ] = useState ( ( ) => new QueryClient ( ) ) ;
3 ...
4 return (
5 < trpc.Provider client = { trpcClient ( ) } queryClient = { queryClient } >
6 < QueryClientProvider client = { queryClient } >
7 < section className = " min-h-screen h-full w-full " > { children } </ section >
8 </ QueryClientProvider >
9 </ trpc.Provider >
10 )
Rakkasjs has layouts , like the ones in nextjs app router which lets you wrap multiple pages with a commone layout , the one at the root src/routes/layout.tsx wraps the whole app so we can put our providers there Let's add some tailwind for styling too official guide
try
or manually
1 pnpm i -D tailwindcss postcss autoprefixer daisyui tailwindcss-animate tailwind-scrollbar tailwindcss-elevation prettier-plugin-tailwindcss
Now add the tailwind config content paths
1
2 export default {
3 content : [
4 "./index.html" ,
5 "./src/**/*.{js,ts,jsx,tsx}" ,
6 ] ,
7 theme : {
8 extend : { } ,
9 } ,
10 plugins : [ ] ,
11 }
And include the base CSS in our layout
src/routes/index.css
1 @tailwind base ;
2 @tailwind components ;
3 @tailwind utilities ;
and now we consume our trpc endpoint
1
2 import { trpc } from "@/utils/trpc" ;
3
4 export default function HomePage ( ) {
5 const query = trpc . hello . hey . useQuery ( ) ;
6 return (
7 < main className = " flex flex-col gap-2 items-center " >
8 < h1 > Hello world! </ h1 >
9 < h3 > { query ?. data } </ h3 >
10 </ main >
11 ) ;
12 }
13
Taddah, Your app is now ready
Alt text let's add the Prisma parts, first thing we need is a schema and the env variables prisma/schema.prisma
1
2
3
4 generator client {
5 provider = "prisma-client-js"
6 }
7
8 generator zod {
9 provider = "zod-prisma-types"
10 }
11
12 datasource db {
13 provider = "sqlite"
14 url = env ( "DATABASE_URL" )
15 }
16
17 model User {
18 id String @ id @ default ( cuid ( ) )
19 name String
20 email String
21 password String
22 createdAt DateTime @ default ( now ( ) )
23 updatedAt DateTime @ updatedAt
24 }
25
26 model Post {
27 id String @ id @ default ( cuid ( ) )
28 createdAt DateTime @ default ( now ( ) )
29 updatedAt DateTime @ updatedAt
30
31 title String
32 body String
33 }
34
35
1 # .env
2 DATABASE_URL="file:./db.sqlite"
then we run the generate command
1 npx prisma generate
2 npx prisma migrate dev
and now we create a helper function for the db
1
2 import { PrismaClient } from "@prisma/client" ;
3 import { envs } from "@/utils/env" ;
4
5 const globalForPrisma = globalThis as unknown as {
6 prisma : PrismaClient | undefined ;
7 } ;
8
9 export const prisma =
10 globalForPrisma . prisma ??
11 new PrismaClient ( {
12 log : envs . DEV_MODE ? [ "query" , "error" , "warn" ] : [ "error" ] ,
13
14 } ) ;
15
16
17 if ( envs . PROD_MODE ) globalForPrisma . prisma = prisma ;
18
and we add prisma into our trpc context
1 export function createTRPCContext ( { req , resHeaders } : FetchCreateContextFnOptions ) {
2 const user = { name : req . headers . get ( 'username' ) ?? 'anonymous' } ;
3
4 return { req , resHeaders , user , prisma } ;
5 }
We can now use it in our trpc routes
1
2
3 import { createTRPCRouter , publicProcedure } from "../trpc" ;
4 import { z } from "zod" ;
5
6 export const PostSchema = z . object ( {
7 id : z . string ( ) . cuid ( ) . optional ( ) ,
8 createdAt : z . coerce . date ( ) . optional ( ) ,
9 updatedAt : z . coerce . date ( ) . optional ( ) ,
10 title : z . string ( ) ,
11 body : z . string ( ) ,
12 } )
13
14 export type Post = z . infer < typeof PostSchema >
15
16 export const postRouter = createTRPCRouter ( {
17
18 getFullList : publicProcedure . query ( ( { ctx } ) => {
19 const posts = ctx . prisma . post . findMany ( ) ;
20 return posts ;
21 } ) ,
22
23 create : publicProcedure
24 . input ( PostSchema )
25 . mutation ( ( { ctx , input } ) => {
26 const post = ctx . prisma . post . create ( {
27 data : input ,
28 } ) ;
29 return post ;
30 } ) ,
31
32 update : publicProcedure
33 . input ( PostSchema )
34 . mutation ( ( { ctx , input } ) => {
35 const post = ctx . prisma . post . update ( { where : { id : input . id } , data : input , } ) ;
36 return post ;
37 } ) ,
38 delete : publicProcedure
39 . input ( z . object ( { id : z . string ( ) } ) )
40 . mutation ( ( { ctx , input } ) => {
41 const post = ctx . prisma . post . delete ( { where : { id : input . id } } ) ;
42 return post ;
43 } )
44 } )
45
46
47 export const appRouter = createTRPCRouter ( {
48 hello : helloRouter ,
49 posts : postRouter
50 } ) ;
51
52
We can now consume it in the frontend
1
2 const query = trpc . posts . getFullList . useQuery ( ) ;
3 return (
4 < main className = " flex flex-col gap-2 items-center " >
5 < h1 > Hello world! </ h1 >
6 < h3 > { query ?. data . map ( ... ) } </ h3 >
7 </ main >
8 ) ;
9
That's all you need to get started, but we can optimize this further For starters, we're not taking full advantage of the SSR step to preload the data.
Rakkasjs hasit;s built-in useQuery and useServerSideQuery hooks to preload the data during the SSR step.
but since we're using tanstack react query we can use the integration guide the creator put out that lets you prefetch the data in the SSR step
we do this by adding Rakkasjs hoos hooks
client-entry.tsx : Code here will run on/wrap every page We can move the providers from the layout to here1
2 import { startClient } from "rakkasjs" ;
3 import { QueryClientProvider , QueryClient } from "@tanstack/react-query" ;
4 import { trpc } from "./utils/trpc" ;
5 import { trpcClient } from "./utils/client" ;
6
7
8 const queryClient = new QueryClient ( {
9 defaultOptions : {
10 queries : {
11 suspense : true ,
12 staleTime : 100 ,
13 refetchOnWindowFocus : false ,
14 refetchOnReconnect : false ,
15 } ,
16 } ,
17 } ) ;
18
19 function setQueryData ( data : Record < string , unknown > ) {
20 for ( const [ key , value ] of Object . entries ( data ) ) {
21 queryClient . setQueryData ( JSON . parse ( key ) , value , { updatedAt : Date . now ( ) } ) ;
22 }
23 }
24
25 declare global {
26 var $ TQD : Record < string , unknown > | undefined ;
27 var $ TQS : typeof setQueryData ;
28 }
29
30
31 setQueryData ( globalThis . $ TQD ?? { } ) ;
32
33 delete globalThis . $ TQD ;
34
35 globalThis . $ TQS = setQueryData ;
36
37 startClient ( {
38 hooks : {
39 wrapApp ( app ) {
40 return (
41 < trpc.Provider client = { trpcClient ( ) } queryClient = { queryClient } >
42 < QueryClientProvider client = { queryClient } > { app } </ QueryClientProvider >
43 </ trpc.Provider >
44 ) ;
45 } ,
46 } ,
47 } ) ;
48
hattip-entry.tsx We also need to create server-side providers1 import { RequestContext , createRequestHandler } from "rakkasjs" ;
2 import { cookie } from "@hattip/cookie" ;
3
4 import {
5 QueryCache ,
6 QueryClient ,
7 QueryClientProvider ,
8 } from "@tanstack/react-query" ;
9 import { uneval } from "devalue" ;
10 import { trpc } from "./utils/trpc" ;
11 import { trpcClient } from "./utils/client" ;
12
13 declare module "rakkasjs" {
14 interface ServerSideLocals {
15 session : any ;
16 }
17 }
18
19 type CreateRequestHandlerParams = Parameters < typeof createRequestHandler > ;
20
21 const attachSession = async ( ctx : RequestContext ) => {
22 try {
23
24
25
26
27
28 ctx . locals . session = {
29 user : "jeffery" ,
30 } ;
31 } catch ( error ) {
32 throw new Error ( "Failed to attach session" ) ;
33 }
34 } ;
35 const logger = async ( ctx : RequestContext ) => {
36 try {
37 console . log ( "========" , ctx . ip , "=============" ) ;
38 } catch ( error ) {
39 throw new Error ( "Failed to attach session" ) ;
40 }
41 } ;
42
43 export default createRequestHandler ( {
44 middleware : {
45
46
47
48 beforePages : [ cookie ( ) , attachSession ] ,
49
50
51
52 beforeApiRoutes : [ logger ] ,
53
54
55
56 beforeNotFound : [ ] ,
57 } ,
58
59 createPageHooks ( ctx ) {
60 let queries = Object . create ( null ) ;
61 console . log ( "hattip ctx" , ctx . request . url ) ;
62 return {
63 wrapApp ( app ) {
64 const queryCache = new QueryCache ( {
65 onSuccess ( data , query ) {
66
67 queries [ query . queryHash ] = data ;
68 } ,
69 } ) ;
70
71 const queryClient = new QueryClient ( {
72 queryCache ,
73 defaultOptions : {
74 queries : {
75 suspense : true ,
76 staleTime : Infinity ,
77 refetchOnWindowFocus : false ,
78 refetchOnReconnect : false ,
79 } ,
80 } ,
81 } ) ;
82
83 return (
84 < trpc.Provider
85 client = { trpcClient ( ctx . request . url ) }
86 queryClient = { queryClient }
87 >
88 < QueryClientProvider client = { queryClient } >
89 { app }
90 </ QueryClientProvider >
91 </ trpc.Provider >
92 ) ;
93 } ,
94
95 emitToDocumentHead ( ) {
96 return ` <script>$TQD=Object.create(null);$TQS=data=>Object.assign($TQD,data);</script> ` ;
97 } ,
98
99 emitBeforeSsrChunk ( ) {
100 if ( Object . keys ( queries ) . length === 0 ) return "" ;
101
102
103
104
105 const queriesString = uneval ( queries ) ;
106 queries = Object . create ( null ) ;
107 return ` <script>$TQS( ${ queriesString } )</script> ` ;
108 } ,
109 } ;
110 } ,
111 } ) ;
112
src/common-hooks.tsx : is also an option but we won't use it hereThe hooks also allow you to define middleware and locals for things like sessions which can be shared between pages and API routes
That's it, there's also a CLI, simply run