Skip to main content
Sonamu provides complete type safety from client to server to database with a single Entity definition. This document explains how types flow through the entire stack.

Type Flow Overview

Compile Time

Type checking with TypeScript - Prevent errors during development

Runtime

Actual data validation with Zod - Block invalid data

Database

PostgreSQL constraints - Guarantee data integrity

API Boundary

Request/Response validation - Safe data exchange

Layer-by-Layer Type Safety

1. Entity β†’ TypeScript

Entity definitions are converted to TypeScript types.
{
  "id": "User",
  "props": [
    { "name": "email", "type": "string", "length": 100 },
    { "name": "age", "type": "integer", "nullable": false },
    { "name": "role", "type": "enum", "id": "UserRole" }
  ],
  "enums": {
    "UserRole": {
      "admin": "Administrator",
      "normal": "Normal User"
    }
  }
}
Type safety guaranteed:
// βœ… Compile success
const user: User = {
  email: "john@test.com",
  age: 30,
  role: "admin",
};

// ❌ Compile error
const user: User = {
  email: "john@test.com",
  age: "30", // Error: Type 'string' is not assignable to type 'number'
  role: "guest", // Error: Type '"guest"' is not assignable to type 'UserRole'
};

2. Entity β†’ Zod Schema

Entity definitions are converted to Zod schemas for runtime validation.
export const User = z.object({
  email: z.string().max(100),
  age: z.number().int(),
  role: UserRole,
});

3. Entity β†’ PostgreSQL

Entity definitions are converted to PostgreSQL table schemas.
{
  "props": [
    { "name": "email", "type": "string", "length": 100 },
    { "name": "age", "type": "integer", "nullable": false }
  ],
  "indexes": [
    { "type": "unique", "columns": [{ "name": "email" }] }
  ]
}
Database constraints:
-- βœ… Insert success
INSERT INTO users (email, age) VALUES ('john@test.com', 30);

-- ❌ Failure: NULL not allowed
INSERT INTO users (email, age) VALUES ('jane@test.com', NULL);

-- ❌ Failure: UNIQUE violation
INSERT INTO users (email, age) VALUES ('john@test.com', 25);

API Layer Type Safety

Model β†’ API Client

Model methods are converted to type-safe API clients.
@api({ httpMethod: "POST" })
async save(params: UserSaveParams[]): Promise<number[]> {
  // Implementation...
}

Request Validation

API requests are automatically validated with Zod schemas.
In Fastify router
// Auto-generated by Sonamu
fastify.post("/user/save", async (request, reply) => {
  // Automatic validation
  const params = UserSaveParams.array().parse(request.body.params);

  // Use safely with validated types
  const result = await UserModel.save(params);
  return result;
});
On validation failure:
// Request
POST /user/save
{
  "params": [
    { "email": "invalid-email", "age": "30" }
  ]
}

// Response (400 Bad Request)
{
  "error": "Validation failed",
  "issues": [
    {
      "path": ["params", 0, "email"],
      "message": "Invalid email"
    },
    {
      "path": ["params", 0, "age"],
      "message": "Expected number, received string"
    }
  ]
}

Response Types

API responses are also type-safe.
@api({ httpMethod: "GET" })
async findById(id: number): Promise<User> {
  return this.findById("A", id);
}

TanStack Query Type Safety

TanStack Query hooks also provide complete type safety.
export function useUserById(id: number) {
  return useQuery({
    queryKey: ["User", "findById", id],
    queryFn: () => findUserById(id),
  });
}

export function useSaveUser() {
  return useMutation({
    mutationFn: (params: UserSaveParams[]) => saveUser(params),
  });
}

Subset Type Safety

Subset queries also provide complete type inference.
async findMany<T extends UserSubsetKey>(
  subset: T,
  params?: UserListParams
): Promise<ListResult<UserListParams, UserSubsetMapping[T]>> {
  const { qb } = this.getSubsetQueries(subset);
  
  // Type is automatically determined based on subset
  return this.executeSubsetQuery({ subset, qb, params });
}

Full Stack Type Flow Example

Shows how types flow in actual CRUD operations.
1

1. Entity Definition

user.entity.json
{
  "id": "User",
  "props": [
    { "name": "email", "type": "string", "length": 100 },
    { "name": "age", "type": "integer" },
    { "name": "role", "type": "enum", "id": "UserRole" }
  ],
  "subsets": {
    "A": ["id", "email", "age", "role"]
  }
}
Generated: TypeScript types, Zod schemas, PostgreSQL schemas
2

2. Model Implementation

user.model.ts
class UserModelClass extends BaseModelClass<...> {
  @api({ httpMethod: "POST" })
  async save(params: UserSaveParams[]): Promise<number[]> {
    // Type-safe implementation
    params.forEach(p => wdb.ubRegister("users", p));
    return wdb.ubUpsert("users");
  }
}
Generated: API client, TanStack Query hooks
3

3. Frontend Usage

UserForm.tsx
import { useSaveUser } from "@/services/services.generated";
import { UserSaveParams } from "@/services/user/user.types";

function UserForm() {
  const { mutate } = useSaveUser();

  const handleSubmit = (values: UserSaveParams) => {
    mutate([values]); // βœ… Type checked
  };

  return <form>...</form>;
}
Validation: Compile time (TypeScript) + Runtime (Zod)
4

4. API Request

// Request (auto-serialized)
POST /user/save
{
  "params": [
    { "email": "john@test.com", "age": 30, "role": "admin" }
  ]
}

// Auto Zod validation
const params = UserSaveParams.array().parse(request.body.params);
Validation: Runtime validation with Zod schema
5

5. Database Save

-- Auto-generated query
INSERT INTO users (email, age, role)
VALUES ('john@test.com', 30, 'admin')
RETURNING id;

-- PostgreSQL constraint checks
-- - email: VARCHAR(100) NOT NULL
-- - age: INTEGER NOT NULL
-- - role: CHECK (role IN ('admin', 'normal'))
Validation: PostgreSQL constraints
6

6. Response Return

// Response
{ "data": [1] }  // number[]

// Type inference in frontend
const ids = await saveUser([...]);
// ids: number[]
Type inference: API client guarantees return type

Preventing Type Mismatches

Sonamu’s E2E type safety prevents common type mismatch issues.
Problem: API returns different type than expectedSonamu Solution:
// Explicit return type in Model
@api()
async findById(id: number): Promise<User> {
  return this.findById("A", id);
}

// API client has same type
export async function findUserById(id: number): Promise<User> {
  // ...
}

// Compile error on type mismatch
Problem: Client sends wrong parametersSonamu Solution:
// Share same type definition
export const UserSaveParams = z.object({...});

// Used in Model
async save(params: UserSaveParams[]): Promise<number[]>

// Used in API client
export async function saveUser(params: UserSaveParams[]): Promise<number[]>

// Auto-validation on server
const validated = UserSaveParams.array().parse(request.body.params);
Problem: Hardcoded string doesn’t match EnumSonamu Solution:
// ❌ Wrong way
const role = "guest";  // Compiles but runtime error

// βœ… Correct way
import { UserRole } from "./sonamu.generated";
const role: UserRole = "admin";  // Type checked
Problem: Using nullable field without null checkSonamu Solution:
type User = {
  bio: string | null;  // Nullable explicit
};

// ❌ Compile error
const length = user.bio.length;

// βœ… Forced null check
const length = user.bio?.length ?? 0;

Type Safety Verification

How to verify that type safety is working properly.

Compile Time Verification

// Create type test file
import { expectType } from "tsd";
import type { User, UserSaveParams } from "./user.types";

// Type check
expectType<User>({
  id: 1,
  email: "test@test.com",
  age: 30,
  role: "admin",
});

// ❌ This code should cause compile error
expectType<User>({
  id: 1,
  email: "test@test.com",
  age: "30", // Type error
});

Runtime Verification

import { describe, it, expect } from "vitest";
import { User, UserSaveParams } from "./user.types";

describe("User type validation", () => {
  it("should validate correct user", () => {
    const result = User.safeParse({
      email: "test@test.com",
      age: 30,
      role: "admin",
    });

    expect(result.success).toBe(true);
  });

  it("should reject invalid user", () => {
    const result = User.safeParse({
      email: "invalid-email",
      age: "30",
      role: "guest",
    });

    expect(result.success).toBe(false);
  });
});

Next Steps