Learn how to call APIs type-safely using the generated Namespace Services.
Service Usage Overview
Namespace Calls UserService.getUser() Concise syntax
Type Safe Auto-completion Compile validation
Subset Support Only needed fields Performance optimization
TanStack Query useUser Hook Auto caching
Basic Usage
Service Import
Generated Services are exported as Namespaces from services.generated.ts.
// β
Correct import method
import { UserService , PostService } from "@/services/services.generated" ;
Benefits of single file import :
Consistency : All Services managed in one place
Easy import : No need to find file paths
Auto update : Auto-synced on pnpm generate
Clear naming : Grouped by Namespace, no conflicts
Basic API Calls
Call Service static functions directly.
import { UserService } from "@/services/services.generated" ;
// Get user (only basic info with Subset "A")
const user = await UserService . getUser ( "A" , 123 );
console . log ( user . username ); // Type safe!
// Update user
await UserService . updateProfile ({
username: "newname" ,
bio: "Hello, World!" ,
});
Features :
Async call with await
Response is auto-parsed (handled inside fetch function)
Types fully guaranteed
Subset parameter required (for getUser, etc.)
Namespace-based structure :
β Class instance: No need for new UserService()
β
Static function: Call UserService.getUser() directly
All functions work independently
Understanding the Subset System
Sonamuβs core feature, the Subset system.
// Subset "A": Basic info
const basicUser = await UserService . getUser ( "A" , 123 );
console . log ( basicUser . id ); // β
OK
console . log ( basicUser . username ); // β
OK
console . log ( basicUser . bio ); // β Compile error (not in Subset A)
// Subset "B": Including bio
const userWithBio = await UserService . getUser ( "B" , 123 );
console . log ( userWithBio . bio ); // β
OK
// Subset "C": All fields
const fullUser = await UserService . getUser ( "C" , 123 );
console . log ( fullUser . createdAt ); // β
OK
Why use Subset :
Performance : Query only needed fields to reduce network cost
Type safety : Returns accurate type for each Subset
Explicitness : Clearly express what data is needed in code
Using in React
TanStack Query Hook (Recommended)
The easiest way is to use auto-generated TanStack Query Hooks.
import { UserService } from "@/services/services.generated" ;
function UserProfile ({ userId } : { userId : number }) {
// Use auto-generated Hook
const { data : user , error , isLoading } = UserService . useUser ( "A" , userId );
if ( isLoading ) return < div > Loading ...</ div > ;
if ( error ) return < div > Error : {error. message } </ div > ;
if ( ! user ) return < div > User not found </ div > ;
return (
< div >
< h1 >{user. username } </ h1 >
< p >{user. email } </ p >
</ div >
);
}
Benefits of TanStack Query Hooks :
Auto caching (reuses same userId)
Auto revalidation (on focus, reconnection)
Auto loading/error state management
Auto duplicate request removal
Function Component (useEffect)
You can also call directly without using Hooks.
import { useState , useEffect } from "react" ;
import { UserService } from "@/services/services.generated" ;
import type { User } from "@/services/services.generated" ;
function UserProfile ({ userId } : { userId : number }) {
const [ user , setUser ] = useState < User | null >( null );
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState < string | null >( null );
useEffect (() => {
async function fetchUser () {
try {
setLoading ( true );
// Get all fields with Subset "C"
const userData = await UserService . getUser ( "C" , userId );
setUser ( userData );
} catch ( err ) {
setError ( err instanceof Error ? err . message : "Failed to load user" );
} finally {
setLoading ( false );
}
}
fetchUser ();
}, [ userId ]);
if ( loading ) return < div > Loading ...</ div > ;
if ( error ) return < div > Error : { error } </ div > ;
if ( ! user ) return < div > User not found </ div > ;
return (
< div >
< h1 >{user. username } </ h1 >
< p >{user. email } </ p >
</ div >
);
}
Recommended : Use TanStack Query Hooks when possible. They provide auto caching, revalidation, and state management, making code much more concise.
Event Handlers
Call APIs based on user actions.
import { useState } from "react" ;
import { UserService } from "@/services/services.generated" ;
function EditProfile ({ userId } : { userId : number }) {
const [ username , setUsername ] = useState ( "" );
const [ bio , setBio ] = useState ( "" );
const [ saving , setSaving ] = useState ( false );
async function handleSubmit ( e : React . FormEvent ) {
e . preventDefault ();
try {
setSaving ( true );
await UserService . updateProfile ({
username ,
bio ,
});
alert ( "Profile updated!" );
} catch ( error ) {
alert ( "Failed to update profile" );
console . error ( error );
} finally {
setSaving ( false );
}
}
return (
< form onSubmit = { handleSubmit } >
< input
type = "text"
value = { username }
onChange = {(e) => setUsername (e.target.value)}
placeholder = "Username"
/>
< textarea
value = { bio }
onChange = {(e) => setBio (e.target.value)}
placeholder = "Bio"
/>
< button type = "submit" disabled = { saving } >
{ saving ? "Saving..." : "Save" }
</ button >
</ form >
);
}
Using Services in SSR
Sonamu supports SSR based on Vite + React. For SSR, use the SSR-specific Services automatically generated in queries.generated.ts.
What are SSR Services?
Separate from frontend Services (services.generated.ts), SSR-specific Services are automatically generated on the backend.
// api/src/application/queries.generated.ts (auto-generated)
import type { SSRQuery } from "sonamu/ssr" ;
export namespace UserService {
// Returns SSRQuery object (no HTTP request)
export const getUser = < T extends UserSubsetKey >( subset : T , id : number ) : SSRQuery =>
createSSRQuery ( "UserModel" , "findById" , [ subset , id ], [ "User" , "getUser" ]);
export const me = () : SSRQuery =>
createSSRQuery ( "UserModel" , "me" , [], [ "User" , "me" ]);
}
Use these Services when registering SSR routes.
// api/src/ssr/routes.ts
import { registerSSR } from "sonamu/ssr" ;
import { UserService , CompanyService } from "../application/queries.generated" ;
// Company detail page SSR
registerSSR ({
path: "/companies/:companyId" ,
preload : ( params ) => [
// Call Service method β Returns SSRQuery object
UserService . me (),
CompanyService . getCompany ( "A" , Number ( params . companyId )),
],
});
Frontend Service vs SSR Service
Item Frontend Service SSR Service File Location web/src/services/services.generated.tsapi/src/application/queries.generated.tsReturn Value Promise<Data> (HTTP request)SSRQuery (object)Used In Browser (React components) Backend (registerSSR) HTTP Request β
Real API call β Direct backend Model execution
// Frontend Service (browser)
const user = await UserService . getUser ( "A" , 123 ); // HTTP GET /api/user/findById
// SSR Service (backend)
const query = UserService . getUser ( "A" , 123 ); // Returns SSRQuery object
// β Sonamu directly executes UserModel.findById("A", 123)
Key Difference : SSR Services execute backend Model methods directly without HTTP requests, eliminating network overhead.
Learn More
For detailed information about SSR, see the following documents.
Error Handling
SonamuError Handling
Handle errors from Service calls in a type-safe manner.
import { UserService } from "@/services/services.generated" ;
import { isSonamuError } from "@/lib/sonamu.shared" ;
async function updateUser () {
try {
await UserService . updateProfile ({
username: "newname" ,
});
} catch ( error ) {
if ( isSonamuError ( error )) {
// Sonamu error (type safe)
console . log ( "Status:" , error . code );
console . log ( "Message:" , error . message );
// Zod validation errors
error . issues . forEach (( issue ) => {
console . log ( ` ${ issue . path . join ( "." ) } : ${ issue . message } ` );
});
// Handle by HTTP status code
if ( error . code === 401 ) {
// Auth error
console . log ( "Please login" );
} else if ( error . code === 403 ) {
// Permission error
console . log ( "Permission denied" );
} else if ( error . code === 422 ) {
// Validation error
console . log ( "Invalid data:" , error . issues );
}
} else {
// General error
console . log ( "Network error:" , error );
}
}
}
Error Handling in React
import { isSonamuError } from "@/lib/sonamu.shared" ;
function EditProfile ({ userId } : { userId : number }) {
const [ error , setError ] = useState < string | null >( null );
const [ validationErrors , setValidationErrors ] = useState < Record < string , string >>({});
async function handleSubmit ( data : any ) {
setError ( null );
setValidationErrors ({});
try {
await UserService . updateProfile ( data );
} catch ( err ) {
if ( isSonamuError ( err )) {
setError ( err . message );
// Map Zod validation errors by field
const fieldErrors : Record < string , string > = {};
err . issues . forEach (( issue ) => {
const field = issue . path . join ( "." );
fieldErrors [ field ] = issue . message ;
});
setValidationErrors ( fieldErrors );
} else {
setError ( "An unexpected error occurred" );
}
}
}
return (
< div >
{ error && < div className = "error-message" > { error } </ div > }
< form onSubmit = {(e) => {
e . preventDefault ();
handleSubmit ({ /* data */ });
}} >
< div >
< input name = "username" />
{ validationErrors . username && (
< span className = "error" > {validationErrors. username } </ span >
)}
</ div >
</ form >
</ div >
);
}
Advanced Patterns
Parallel Requests
Call multiple APIs simultaneously to improve performance.
import { UserService , PostService } from "@/services/services.generated" ;
async function loadUserDashboard ( userId : number ) {
// β Sequential execution (slow)
const user = await UserService . getUser ( "A" , userId );
const posts = await PostService . getPostsByUser ( userId );
const comments = await PostService . getCommentsByUser ( userId );
return { user , posts , comments };
}
async function loadUserDashboardFast ( userId : number ) {
// β
Parallel execution (fast)
const [ user , posts , comments ] = await Promise . all ([
UserService . getUser ( "A" , userId ),
PostService . getPostsByUser ( userId ),
PostService . getCommentsByUser ( userId ),
]);
return { user , posts , comments };
}
Performance comparison :
Sequential: 300ms + 200ms + 150ms = 650ms
Parallel: max(300ms, 200ms, 150ms) = 300ms
Subset Optimization
Choose appropriate Subset for the situation.
// List view: Only basic info
const users = await UserService . getUsers ( "A" );
users . map (( user ) => (
< div key = {user. id } >
{ user . username } - { user . email }
</ div >
));
// Detail view: All info
const fullUser = await UserService . getUser ( "C" , userId );
< div >
< h1 >{fullUser. username } </ h1 >
< p >{fullUser. bio } </ p >
< p > Created : { fullUser . createdAt } </ p >
</ div >
TanStack Query Utilization
Conditional Fetching
function UserProfile ({ userId } : { userId : number | null }) {
const { data : user } = UserService . useUser (
"A" ,
userId ! ,
{ enabled: userId !== null } // Don't call if userId is null
);
if ( ! userId ) return < div > Please select a user </ div > ;
if ( ! user ) return < div > Loading ...</ div > ;
return < div >{user. username } </ div > ;
}
Cache Invalidation
import { useQueryClient } from "@tanstack/react-query" ;
import { UserService } from "@/services/services.generated" ;
function EditProfile ({ userId } : { userId : number }) {
const queryClient = useQueryClient ();
async function handleUpdate ( data : any ) {
await UserService . updateProfile ( data );
// Invalidate specific query
queryClient . invalidateQueries (
UserService . getUserQueryOptions ( "A" , userId )
);
// Or invalidate all User queries
queryClient . invalidateQueries ({
queryKey: [ "User" ],
});
}
}
Prefetching
function UserList ({ userIds } : { userIds : number [] }) {
const queryClient = useQueryClient ();
// Preload on hover
function handleMouseEnter ( userId : number ) {
queryClient . prefetchQuery (
UserService . getUserQueryOptions ( "A" , userId )
);
}
return (
< ul >
{ userIds . map (( id ) => (
< li
key = { id }
onMouseEnter = {() => handleMouseEnter ( id )}
>
User { id }
</ li >
))}
</ ul >
);
}
Practical Example
Complete CRUD Flow
import { useState } from "react" ;
import { UserService } from "@/services/services.generated" ;
import { isSonamuError } from "@/lib/sonamu.shared" ;
function UserManagement () {
// List query (Subset A)
const { data : users , refetch } = UserService . useUsers ( "A" );
// Create
async function handleCreate ( data : { username : string ; email : string }) {
try {
await UserService . create ( data );
refetch (); // Refresh list
alert ( "Created!" );
} catch ( error ) {
if ( isSonamuError ( error )) {
alert ( error . message );
}
}
}
// Update
async function handleUpdate ( id : number , data : { username : string }) {
try {
await UserService . update ( id , data );
refetch ();
alert ( "Updated!" );
} catch ( error ) {
if ( isSonamuError ( error )) {
alert ( error . message );
}
}
}
// Delete
async function handleDelete ( id : number ) {
if ( ! confirm ( "Are you sure?" )) return ;
try {
await UserService . delete ( id );
refetch ();
alert ( "Deleted!" );
} catch ( error ) {
if ( isSonamuError ( error )) {
alert ( error . message );
}
}
}
return (
< div >
< h1 > Users </ h1 >
{ users ?. map (( user ) => (
< div key = {user. id } >
{ user . username }
< button onClick = {() => handleUpdate (user.id, { username : "new" })} >
Edit
</ button >
< button onClick = {() => handleDelete (user.id)} >
Delete
</ button >
</ div >
))}
</ div >
);
}
Cautions
Cautions when using Services :
Never manually modify generated Service files (services.generated.ts)
Subset parameter required : Must specify subset for getUser, etc.
In Server Components, call backend model directly instead of Service
Use isSonamuError() type guard for error handling
TanStack Query Hooks should only be called inside components
await keyword required (all Service functions are async)
Itβs a Namespace so no new needed : Call UserService.getUser() directly
Next Steps