Orval Templates
Orval templates use OpenAPI specifications to generate fully typed API clients with React Query hooks. This approach provides excellent documentation, type safety, and developer experience.
Available Orval Templates
Full Stack with Orval
Complete application with all features enabled.
npm create vinoflare@latest my-app
Features:
✅ React frontend with TanStack Router
✅ Orval API client generation
✅ Cloudflare D1 database with Drizzle ORM
✅ Better Auth with Discord OAuth
✅ Full type safety across the stack
Orval without Authentication
Perfect for public-facing applications that need data persistence.
npm create vinoflare@latest my-app --no-auth
Features:
✅ React frontend with TanStack Router
✅ Orval API client generation
✅ Cloudflare D1 database with Drizzle ORM
❌ No authentication
✅ Public API endpoints
Orval without Database
Ideal for stateless frontends that interact with external APIs.
npm create vinoflare@latest my-app --no-db
Features:
✅ React frontend with TanStack Router
✅ Orval API client generation
❌ No database
❌ No authentication
✅ Lightweight and fast
How Orval Works
1. OpenAPI Schema
Your API endpoints are documented in an OpenAPI schema:
# openapi.yaml
paths :
/api/todos :
get :
operationId : getTodos
responses :
'200' :
description : List of todos
content :
application/json :
schema :
type : array
items :
$ref : '#/components/schemas/Todo'
2. Code Generation
Orval generates typed hooks and client functions:
This creates:
Typed API client functions
React Query hooks
TypeScript interfaces
Request/response types
3. Using Generated Hooks
import { useGetTodos, useCreateTodo } from "@/generated/endpoints" ;
function TodoList () {
// Fully typed query hook
const { data : todos , isLoading } = useGetTodos ();
// Fully typed mutation hook
const createTodo = useCreateTodo ();
const handleCreate = () => {
createTodo. mutate ({
title: "New Todo" ,
completed: false
});
};
return (
// Your component JSX
);
}
Project Structure
Configuration
Orval Config
The orval.config.ts
file controls code generation:
export default {
api: {
input: {
target: "./src/generated/openapi.json" ,
},
output: {
target: "./src/generated/endpoints" ,
client: "react-query" ,
override: {
mutator: {
path: "./src/client/lib/custom-fetch.ts" ,
name: "customFetch" ,
},
},
},
} ,
} ;
Custom Fetch
Configure authentication and base URL:
// src/client/lib/custom-fetch.ts
export const customFetch = async < T >(
url : string ,
options ?: RequestInit
) : Promise < T > => {
const response = await fetch (url, {
... options,
credentials: "include" , // Include cookies
headers: {
"Content-Type" : "application/json" ,
... options?.headers,
},
});
if ( ! response.ok) {
throw new Error ( `API Error: ${ response . statusText }` );
}
return response. json ();
};
Development Workflow
1. Define API Routes
// src/server/modules/todos/todos.routes.ts
import { createRouter } from "@/server/core/api-builder" ;
import { z } from "zod" ;
const todoSchema = z. object ({
id: z. string (),
title: z. string (),
completed: z. boolean (),
});
export const todosRouter = createRouter ()
. get ( "/" , {
responses: {
200 : {
description: "List of todos" ,
schema: z. array (todoSchema),
},
},
handler : async ( c ) => {
const todos = await db. select (). from (todosTable);
return c. json (todos);
},
})
. post ( "/" , {
body: todoSchema. omit ({ id: true }),
responses: {
201 : {
description: "Created todo" ,
schema: todoSchema,
},
},
handler : async ( c ) => {
const body = await c.req. valid ( "json" );
const todo = await db. insert (todosTable). values (body);
return c. json (todo, 201 );
},
});
2. Generate OpenAPI Schema
3. Generate Client Code
4. Use in Frontend
import { useGetTodos } from "@/generated/endpoints" ;
export function TodosPage () {
const { data : todos , isLoading , error } = useGetTodos ();
if (isLoading) return < div >Loading ...</ div > ;
if (error) return < div >Error: {error.message} </ div > ;
return (
< ul >
{ todos ?. map (( todo ) => (
< li key = {todo.id} > {todo.title} </ li >
))}
</ ul >
);
}
Best Practices
1. API Design
Follow RESTful conventions
Use consistent naming
Document all endpoints
Version your API
2. Schema Management
Keep schemas DRY with $ref
Use meaningful operation IDs
Document response types
Validate request bodies
3. Error Handling
const { data , error , isLoading } = useGetTodos ({
query: {
onError : ( error ) => {
console. error ( "Failed to fetch todos:" , error);
toast. error ( "Could not load todos" );
},
},
});
4. Optimistic Updates
const queryClient = useQueryClient ();
const createTodo = useCreateTodo ({
mutation: {
onMutate : async ( newTodo ) => {
// Cancel outgoing refetches
await queryClient. cancelQueries ({ queryKey: [ "getTodos" ] });
// Snapshot previous value
const previousTodos = queryClient. getQueryData ([ "getTodos" ]);
// Optimistically update
queryClient. setQueryData ([ "getTodos" ], ( old ) => [
... old,
{ ... newTodo, id: "temp-id" },
]);
return { previousTodos };
},
onError : ( err , newTodo , context ) => {
// Rollback on error
queryClient. setQueryData ([ "getTodos" ], context.previousTodos);
},
onSettled : () => {
// Always refetch after error or success
queryClient. invalidateQueries ({ queryKey: [ "getTodos" ] });
},
},
});
Common Patterns
// API endpoint
. get ( "/" , {
query: z. object ({
page: z. number (). default ( 1 ),
limit: z. number (). default ( 10 ),
}),
handler : async ( c ) => {
const { page , limit } = c.req. valid ( "query" );
const offset = (page - 1 ) * limit;
const todos = await db
. select ()
. from (todosTable)
. limit (limit)
. offset (offset);
return c. json ({
data: todos,
page,
limit,
total: await db. count (todosTable),
});
},
})
// Frontend usage
const { data } = useGetTodos ({
page: currentPage,
limit: 10 ,
});
Search and Filtering
// API endpoint
. get ( "/" , {
query: z. object ({
search: z. string (). optional (),
status: z. enum ([ "all" , "active" , "completed" ]). default ( "all" ),
}),
handler : async ( c ) => {
const { search , status } = c.req. valid ( "query" );
let query = db. select (). from (todosTable);
if (search) {
query = query. where ( like (todosTable.title, `%${ search }%` ));
}
if (status !== "all" ) {
query = query. where (
eq (todosTable.completed, status === "completed" )
);
}
return c. json ( await query);
},
})
Troubleshooting
Generated types not updating?
Check that OpenAPI schema is generated: npm run gen:openapi
Regenerate client code: npm run gen:api
Restart TypeScript server in your editor
API calls failing?
Check browser DevTools Network tab
Verify API is running: npm run dev
Check CORS settings if cross-origin
Ensure authentication cookies are included
Type errors in generated code?
Ensure OpenAPI schema is valid
Check for circular references
Verify all schemas are properly defined
Update Orval to latest version
Next Steps