Full-Stack Demo: User Service

A complete backend service with typed endpoints - save this first!

/#
## User Service

A complete backend service running in the browser.
Save this module as "user-service", then run the client example.

Features:
- Type-safe endpoints with validation
- In-memory data store
- Full CRUD operations
#/

// In-memory store (would be a real DB in production)
const users = new Map()
let nextId = 1

// Create a new user
export function createUser(input: {
  name: 'Alice',
  email: 'alice@example.com'
}):! { id: 0, name: '', email: '', createdAt: '' } {
  const user = {
    id: nextId++,
    name: input.name,
    email: input.email,
    // Raw `Date` is not allowed in .tjs; LegacyDate is the named escape.
    createdAt: LegacyDate().toISOString()
  }
  users.set(user.id, user)
  return user
}

// Get user by ID (null if there is no such user)
export function getUser(input: { id: 1 }):! { id: 0, name: '', email: '', createdAt: '' } | null {
  return users.get(input.id) ?? null
}

// Update a user (null if there is no such user)
export function updateUser(input: {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
}):! { id: 0, name: '', email: '', createdAt: '' } | null {
  const existing = users.get(input.id)
  if (!existing) return null

  const updated = { ...existing, name: input.name, email: input.email }
  users.set(input.id, updated)
  return updated
}

// Delete a user
export function deleteUser(input: { id: 1 }):! { success: true, deleted: 0 } {
  const existed = users.has(input.id)
  users.delete(input.id)
  return { success: existed, deleted: existed ? input.id : 0 }
}

// List all users
export function listUsers(input: { limit: 10, offset: 0 }):! { users: [{ id: 0, name: '', email: '', createdAt: '' }], total: 0 } {
  const all = [...users.values()]
  const slice = all.slice(input.offset, input.offset + input.limit)
  return { users: slice, total: all.length }
}

// Search users by name
export function searchUsers(input: { query: '' }):! { users: [{ id: 0, name: '', email: '', createdAt: '' }] } {
  const query = input.query.toLowerCase()
  const matches = [...users.values()].filter(u =>
    u.name.toLowerCase().includes(query)
  )
  return { users: matches }
}

// Test the service
test 'createUser creates user with ID' {
  const user = createUser({ name: 'Test', email: 'test@test.com' })
  expect(user.id).toBeGreaterThan(0)
  expect(user.name).toBe('Test')
}

test 'getUser returns created user' {
  const created = createUser({ name: 'Bob', email: 'bob@test.com' })
  const fetched = getUser({ id: created.id })
  expect(fetched.name).toBe('Bob')
}

test 'updateUser modifies user' {
  const user = createUser({ name: 'Original', email: 'orig@test.com' })
  const updated = updateUser({ id: user.id, name: 'Updated', email: 'new@test.com' })
  expect(updated.name).toBe('Updated')
}

test 'deleteUser removes user' {
  const user = createUser({ name: 'ToDelete', email: 'del@test.com' })
  const result = deleteUser({ id: user.id })
  expect(result.success).toBe(true)
  expect(getUser({ id: user.id })).toBe(null)
}

test 'a wrong input type is an error value, not a crash' {
  const bad = createUser({ name: 123, email: 'x@example.com' })
  expect(bad instanceof Error).toBe(true)
}

// Demo
console.log('=== User Service Demo ===\n')

const alice = createUser({ name: 'Alice', email: 'alice@company.com' })
console.log('Created:', alice)

const bob = createUser({ name: 'Bob', email: 'bob@company.com' })
console.log('Created:', bob)

const carol = createUser({ name: 'Carol', email: 'carol@company.com' })
console.log('Created:', carol)

console.log('\nAll users:', listUsers({ limit: 10, offset: 0 }))
console.log('\nSearch "ob":', searchUsers({ query: 'ob' }))

// Type validation in action: the call returns an error VALUE
console.log('\nType validation test:')
const bad = createUser({ name: 123, email: 'x@example.com' })
console.log('createUser({ name: 123 }) ->', bad.message)

What to notice

A typed endpoint with defaults

Optional query parameters are the other half of most endpoints. An object-literal default in TJS is a dictionary: each member has its own default, and a partial argument is merged with them, so a caller can send only what it cares about:

const items = Array.from({ length: 25 }, (_, i) => `item ${i + 1}`)

function page(query = { limit: 10, offset: 0 }):! { items: [''], total: 0 } {
  const { limit, offset } = query
  return { items: items.slice(offset, offset + limit), total: items.length }
}

console.log('page()                ->', page().items.join(', '))
console.log('page({ offset: 20 })  ->', page({ offset: 20 }).items.join(', '))
console.log('page({ limit: 2 })    ->', page({ limit: 2 }).items.join(', '))

// Members are type-checked, so a string offset is refused before the body runs.
const bad = page({ offset: 'ten' })
console.log('page({ offset: "ten" }) ->', bad instanceof Error ? bad.message : bad)

page({ offset: 20 }) gets limit: 10 from the default; JavaScript would have replaced the whole default object and left limit undefined. See Dictionary Defaults for the full rules.

Limits

Save this module as user-service in the playground, then run Full-Stack Demo: Client App, which imports it.