> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Customizing Generated Code

> How to safely customize auto-generated files without modifying them directly

If you directly modify code auto-generated by Sonamu, it will be overwritten on the next regeneration. This document explains how to safely customize generated code.

## Basic Principles

<CardGroup cols={2}>
  <Card title="Generated Files are Read-Only" icon="lock">
    Never modify `*.generated.*` files Changes lost on auto-regeneration
  </Card>

  <Card title="Customize Through Extension" icon="puzzle-piece">
    Wrap or extend in separate files Safe and maintainable
  </Card>

  <Card title="Control at Source" icon="sliders">
    Adjust in Entity, Model, Types Preserved across regenerations
  </Card>

  <Card title="Leverage Templates" icon="file-code">
    Control generation with custom templates Reflect project requirements
  </Card>
</CardGroup>

## Modifiable vs Non-Modifiable Files

### ❌ Non-Modifiable Files

**Never modify** these files. They are overwritten on regeneration.

| File Pattern            | Regeneration Trigger | Example                      |
| ----------------------- | -------------------- | ---------------------------- |
| `*.generated.ts`        | Entity/Model changes | `sonamu.generated.ts`        |
| `*.generated.sso.ts`    | Entity changes       | `sonamu.generated.sso.ts`    |
| `*.generated.tsx`       | Model changes        | `entry-server.generated.tsx` |
| `*.generated.http`      | Model changes        | `sonamu.generated.http`      |
| `services.generated.ts` | Model changes        | `services.generated.ts`      |
| `queries.generated.ts`  | Model changes        | `queries.generated.ts`       |
| `sd.generated.ts`       | Entity/i18n changes  | `sd.generated.ts`            |

### ✅ Modifiable Files

These files **can be modified** after initial generation.

| File Pattern        | Regeneration             | Example          |
| ------------------- | ------------------------ | ---------------- |
| `{entity}.types.ts` | Not after first creation | `user.types.ts`  |
| `{entity}.model.ts` | Not after scaffold       | `user.model.ts`  |
| `{Entity}List.tsx`  | Not after scaffold       | `UserList.tsx`   |
| `{Entity}Form.tsx`  | Not after scaffold       | `UserForm.tsx`   |
| Custom files        | Never regenerated        | `user.custom.ts` |

## Customizing API Clients

Don't modify `services.generated.ts` directly—wrap it instead.

### ❌ Wrong Approach

```typescript title="services.generated.ts" theme={null}
// ❌ Don't modify directly!
export async function findUserById(id: number): Promise<User> {
  // Adding custom logic
  console.log("Finding user:", id);

  const { data } = await axios.get("/user/findById", { params: { id } });
  return data;
}
// Will be overwritten on next Model change 💥
```

### ✅ Correct Approach 1: Wrapper Functions

<CodeGroup>
  ```typescript title="services/user/user.service.ts (create new)" theme={null}
  import { findUserById as _findUserById } from "../services.generated";

  // Extend with wrapper function
  export async function findUserById(id: number) {
  console.log("Finding user:", id);

  // Call original
  const user = await \_findUserById(id);

  // Additional processing
  if (!user.email_verified) {
  throw new Error("Email not verified");
  }

  return user;
  }

  // Re-export other functions as-is
  export {
  findManyUsers,
  saveUser,
  deleteUser,
  } from "../services.generated";

  ```

  ```tsx title="Usage" theme={null}
  // Use custom service
  import { findUserById, findManyUsers } from "@/services/user/user.service";

  function UserProfile({ userId }) {
    const { data } = useQuery({
      queryKey: ["user", userId],
      queryFn: () => findUserById(userId),  // Includes custom logic
    });
  }
  ```
</CodeGroup>

**Benefits**:

* No modification to generated files
* Reuses original functions
* Maintains type safety

### ✅ Correct Approach 2: Axios Interceptor

```typescript title="services/axios-config.ts (create new)" theme={null}
import axios from "axios";

// Request Interceptor
axios.interceptors.request.use((config) => {
  // Common logging
  console.log(`API Request: ${config.method} ${config.url}`);

  // Add auth token
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

// Response Interceptor
axios.interceptors.response.use(
  (response) => response,
  (error) => {
    // Common error handling
    if (error.response?.status === 401) {
      // Logout
      localStorage.removeItem("token");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  },
);
```

**Benefits**:

* Automatically applies to all APIs
* Eliminates duplicate code
* Centralized configuration

### ✅ Correct Approach 3: TanStack Query Customization

```typescript title="hooks/useUserQuery.ts (create new)" theme={null}
import { useQuery } from "@tanstack/react-query";
import { findUserById } from "@/services/services.generated";

// Custom Hook
export function useUserById(id: number, options?: UseQueryOptions) {
  return useQuery({
    queryKey: ["user", id],
    queryFn: () => findUserById(id),
    // Additional options
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
    retry: 3,
    // Merge custom options
    ...options,
  });
}

// Usage
function UserProfile({ userId }) {
  const { data, isLoading } = useUserById(userId, {
    // Per-component options
    enabled: userId > 0,
  });
}
```

**Benefits**:

* Centralized management of common options
* Per-component customization possible
* Leverages TanStack Query features

## Customizing Types

`{entity}.types.ts` is not regenerated, so you can modify it freely.

### Adding Custom Types

```typescript title="api/src/application/user/user.types.ts" theme={null}
import { z } from "zod";

// Auto-generated types (modifiable)
export type User = {
  id: number;
  email: string;
  username: string;
};

export const User = z.object({
  id: z.number(),
  email: z.string(),
  username: z.string(),
});

// ✅ Add custom types (safe)
export const UserLoginParams = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  rememberMe: z.boolean().optional(),
});
export type UserLoginParams = z.infer<typeof UserLoginParams>;

export const UserProfileUpdateParams = User.pick({
  username: true,
}).extend({
  bio: z.string().optional(),
  avatar_url: z.string().url().optional(),
});
export type UserProfileUpdateParams = z.infer<typeof UserProfileUpdateParams>;

// Domain-specific types
export type UserWithStats = User & {
  post_count: number;
  follower_count: number;
};

// Enum extension
export const ExtendedUserRole = z.enum([
  ...User.shape.role.options, // Existing roles
  "moderator", // Addition
]);
```

**Note**: `{entity}.types.ts` is copied to targets, so changes are automatically synchronized.

### Enhanced Validation

```typescript title="user.types.ts" theme={null}
// Complex Validation
export const UserSaveParams = z
  .object({
    email: z
      .string()
      .email("Please enter a valid email")
      .refine((email) => !email.endsWith("@temp.com"), "Temporary emails are not allowed"),

    username: z
      .string()
      .min(2, "Minimum 2 characters")
      .max(20, "Maximum 20 characters")
      .regex(/^[a-zA-Z0-9_]+$/, "Only letters, numbers, and underscores allowed"),

    password: z
      .string()
      .min(8)
      .regex(/[A-Z]/, "Must include uppercase letter")
      .regex(/[0-9]/, "Must include number")
      .regex(/[!@#$%^&*]/, "Must include special character"),

    age: z.number().int().min(18, "Must be 18 or older to register").max(100),
  })
  .refine((data) => data.password !== data.username, {
    message: "Password must be different from username",
    path: ["password"],
  });
```

## Customizing Models

Model files can be freely modified after scaffolding.

### Adding Methods

```typescript title="api/src/application/user/user.model.ts" theme={null}
class UserModelClass extends BaseModelClass {
  // Scaffold-generated method (modifiable)
  @api({ httpMethod: "GET" })
  async findById(id: number): Promise<User> {
    // ...
  }

  // ✅ Add custom methods (safe)
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
  async login(params: UserLoginParams): Promise<{ user: User; token: string }> {
    const user = await this.getPuri("r").from("users").where("email", params.email).first();

    if (!user) {
      throw new UnauthorizedException("Email or password does not match");
    }

    const isValid = await bcrypt.compare(params.password, user.password);
    if (!isValid) {
      throw new UnauthorizedException("Email or password does not match");
    }

    const token = jwt.sign({ userId: user.id }, SECRET_KEY);
    return { user, token };
  }

  @api({ httpMethod: "GET", guards: ["user"] })
  async me(): Promise<User | null> {
    const context = Sonamu.getContext();
    if (!context.user) return null;

    return this.findById("A", context.user.id);
  }

  // Internal helper method (no @api)
  async findByEmail(email: string): Promise<User | null> {
    return this.getPuri("r").from("users").where("email", email).first();
  }
}
```

**Added APIs are auto-generated**:

* `login()`, `me()` functions added to `services.generated.ts`
* Test cases added to `sonamu.generated.http`

### Separating Helper Methods

```typescript title="api/src/application/user/user.helpers.ts (create new)" theme={null}
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";

// Password-related
export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, 10);
}

export async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

// Token-related
export function generateToken(userId: number): string {
  return jwt.sign({ userId }, process.env.JWT_SECRET!, {
    expiresIn: "7d",
  });
}

export function verifyToken(token: string): { userId: number } {
  return jwt.verify(token, process.env.JWT_SECRET!) as { userId: number };
}
```

```typescript title="Usage in user.model.ts" theme={null}
import { hashPassword, verifyPassword, generateToken } from "./user.helpers";

class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async register(params: UserRegisterParams) {
    // Use helper
    const hashedPassword = await hashPassword(params.password);

    const [userId] = await this.save([
      {
        ...params,
        password: hashedPassword,
      },
    ]);

    const token = generateToken(userId);
    return { userId, token };
  }
}
```

## Customizing React Components

Components generated via scaffold can be freely modified.

### Customizing Form Components

```tsx title="web/src/pages/user/UserForm.tsx" theme={null}
// Customize after scaffold generation
import { useSaveUser } from "@/services/services.generated";
import { UserSaveParams } from "@/services/user/user.types";

export function UserForm({ userId }: { userId?: number }) {
  const { data: user } = useUserById(userId);
  const { mutate: save, isPending } = useSaveUser();

  // ✅ Custom validation
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    // ✅ Client-side validation
    const email = formData.get("email") as string;
    if (!email.includes("@")) {
      setErrors({ email: "Please enter a valid email" });
      return;
    }

    // Parse with Zod
    const result = UserSaveParams.safeParse({
      email,
      username: formData.get("username"),
    });

    if (!result.success) {
      // Display Zod errors
      setErrors(result.error.flatten().fieldErrors);
      return;
    }

    save([result.data], {
      onSuccess: () => {
        alert("Saved successfully");
        setErrors({});
      },
      onError: (error) => {
        alert(error.message);
      },
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input name="email" type="email" defaultValue={user?.email} required />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <input name="username" defaultValue={user?.username} required />
        {errors.username && <span className="error">{errors.username}</span>}
      </div>

      <button type="submit" disabled={isPending}>
        {isPending ? "Saving..." : "Save"}
      </button>
    </form>
  );
}
```

### Customizing List Components

```tsx title="web/src/pages/user/UserList.tsx" theme={null}
import { useUsers } from "@/services/services.generated";
import { UserSearchInput } from "./UserSearchInput";

export function UserList() {
  const [params, setParams] = useState({
    num: 20,
    page: 1,
    search: "email" as const,
    keyword: "",
  });

  const { data, isLoading } = useUsers(params);

  // ✅ Add custom features
  const handleExport = () => {
    if (!data?.rows) return;

    const csv = [["ID", "Email", "Username"], ...data.rows.map((u) => [u.id, u.email, u.username])]
      .map((row) => row.join(","))
      .join("\n");

    const blob = new Blob([csv], { type: "text/csv" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "users.csv";
    a.click();
  };

  return (
    <div>
      <div className="toolbar">
        <UserSearchInput
          value={params.search}
          onChange={(search) => setParams({ ...params, search })}
        />
        <input
          type="text"
          value={params.keyword}
          onChange={(e) => setParams({ ...params, keyword: e.target.value })}
        />
        <button onClick={handleExport}>Export</button>
      </div>

      <table>{/* ... */}</table>
    </div>
  );
}
```

## Customizing Enums

To add or modify enums, adjust them in the Entity.

### Modifying Enums in Entity

<CodeGroup>
  ```json title="user.entity.json" theme={null}
  {
    "enums": {
      "UserRole": {
        "admin": "Administrator",
        "moderator": "Moderator",      // ✅ Added
        "normal": "Normal User",
        "guest": "Guest"               // ✅ Added
      }
    }
  }
  ```

  ```typescript title="sonamu.generated.ts (auto-regenerated)" theme={null}
  // Auto-generated enum
  export const UserRole = z.enum(["admin", "moderator", "normal", "guest"]);
  export type UserRole = z.infer<typeof UserRole>;

  // Auto-generated label helper
  export function userRoleLabel(role: UserRole): string {
    return {
      admin: "Administrator",
      moderator: "Moderator",
      normal: "Normal User",
      guest: "Guest",
    }[role];
  }
  ```
</CodeGroup>

### Extending Enums (Separate File)

```typescript title="user.types.ts" theme={null}
import { UserRole as _UserRole } from "../sonamu.generated";

// ✅ Extend enum
export const ExtendedUserRole = z.enum([
  ..._UserRole.options,
  "suspended", // Suspended
  "deleted", // Deleted
]);
export type ExtendedUserRole = z.infer<typeof ExtendedUserRole>;

// Custom labels
export function extendedUserRoleLabel(role: ExtendedUserRole): string {
  if (role === "suspended") return "Suspended";
  if (role === "deleted") return "Deleted";

  // Use existing labels
  return userRoleLabel(role as UserRole);
}
```

## Customizing Subsets

To modify subsets, adjust them in the Entity.

### Adding/Removing Subset Fields

<CodeGroup>
  ```json title="user.entity.json" theme={null}
  {
    "subsets": {
      "A": [
        "id",
        "email",
        "username",
        "role",
        "created_at"    // ✅ Added
      ],
      "P": [
        "id",
        "email",
        "employee.id",
        "employee.department.name"
      ],
      "SS": [
        "id",
        "email"
      ],
      "Public": [      // ✅ New Subset added
        "id",
        "username"
      ]
    }
  }
  ```

  ```typescript title="sonamu.generated.sso.ts (auto-regenerated)" theme={null}
  // Auto-generated subset queries
  export const userSubsetQueries = {
    A: (puri) =>
      puri.table("users").select([
        "users.id",
        "users.email",
        "users.username",
        "users.role",
        "users.created_at", // Auto-added
      ]),

    Public: (puri) => puri.table("users").select(["users.id", "users.username"]),
  };
  ```
</CodeGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="When Files Regenerate" icon="clock" href="/en/core-concepts/auto-generation/when-files-regenerate">
    Understand file regeneration timing
  </Card>

  <Card title="Syncer" icon="rotate" href="/en/core-concepts/auto-generation/syncer">
    Deep dive into the Syncer system
  </Card>

  <Card title="Templates" icon="file-code" href="/en/advanced/templates/creating-templates">
    Create custom templates
  </Card>

  <Card title="Testing" icon="flask" href="/en/testing/model-testing/unit-tests">
    Test your customized code
  </Card>
</CardGroup>
