Edit this page

Event-based telefunctions

What is this about?

This page explains how to most efficiently use Telefunc (and RPC in general) to significantly increase development speed.

See Overview > RPC if you aren't familiar with RPC.

With REST and GraphQL, API endpoints are:

  • Generic (agnostic to your frontend needs)
  • Backend-owned (defined and implemented by the backend team)

With Telefunc, it's usually the opposite — telefunctions are:

  • Tailored (specific to your frontend needs)
  • Frontend-owned (defined and implemented by the frontend team)

This inversion is at the cornerstone of using Telefunc efficiently.

You may be tempted to create generic telefunctions but we recommend against it. Instead, we recommend implementing what we call event-based telefunctions.

// database/todo.telefunc.ts
 
// ❌ Generic telefunction: one telefunction re-used for multiple use case
export async function updateTask(id: number, modifications: Partial<TodoItem>) {
  // ...
}
// components/TodoList.telefunc.ts
 
// ✅ Event-based telefunctions: one telefunction per use case
export async function onTodoTextUpdate(id: number, text: string) {
  // ...
}
export async function onTodoCompleted(id: number) {
  // ...
}

In the example below, we explain why event-based telefunctions lead to increased:

  • Development speed (and we explain how to keep things DRY)
  • Security
  • Performance

Example

Imagine an existing to-do list app, and the product manager requests a new feature: add a new button Mark all tasks as completed.

With a RESTful API, the app would typically do this:

HTTP            URL                                           PAYLOAD
=========       =========================================     =====================
# Make a request to fetch all non-completed tasks
GET             https://api.todo.com/task?completed=false
# Make a request per task to update it
POST            https://api.todo.com/task/42                  { "completed": true }
POST            https://api.todo.com/task/1337                { "completed": true }
POST            https://api.todo.com/task/7                   { "completed": true }

This is inefficient as it makes a lot of HTTP requests (the infamous N+1 problem).

With Telefunc, you can do this instead:

// components/TodoList.telefunc.ts
// Environment: server
 
import { Tasks } from '../database/Tasks'
 
export async function onMarkAllAsCompleted() {
  // With an ORM:
  await Tasks.update({ completed: true }).where({ completed: false })
  /* Or with SQL:
  await sql('UPDATE tasks SET completed = true WHERE completed = false')
  */
}

The telefunction onMarkAllAsCompleted() is tailored: it's created specifically to serve the needs of the <TodoList> component. It's simpler and a lot more efficient.

Convention

We recommend naming telefunctions onSomeEvent() (see Naming convention), because telefunction calls are always triggered by some kind of event — typically a user action, such as the user clicking on a button.

# Also: we recommend co-locating .telefunc.js files
components/TodoList.telefunc.ts # telefunctions for <TodoList>
components/TodoList.tsx # <TodoList> implementation
// components/TodoList.tsx
// Environment: client
 
import { onMarkAllAsCompleted } from './TodoList.telefunc.ts'
 
function TodoList() {
  return <>
    {/* ... */}
    <button onClick={onMarkAllAsCompleted}>
      Mark all as completed
    </button>
  </>
}

This naming convention ensures telefunctions are tightly coupled to UI components.

With Telefunc, you think in terms of what the frontend needs (instead of thinking of the backend as a generic data provider). From that perspective, it makes more sense to co-locate telefunctions next to UI components (instead of next to where data comes from).

Too restrictive convention?

To keep things DRY you may be tempted to define a single telefunction that is re-used by many UI components. For example:

// database/actions/tasks.telefunc.ts
// Environment: server
 
import { Task } from '../models/Task'
import { getContext } from 'telefunc'
 
// One telefunction used by multiple UI components
export async function updateTask(id: number, mods: Partial<typeof Task>) {
  const { user } = getContext()
  const task = await Task.update(mods).where({ id, author: user.id })
  // Returns the updated value task.modifiedAt
  return task
}

But this generic telefunction has two issues:

  1. It isn't safe.

    As explained at Overview > RPC, telefunctions are public. This means any user can call updateTask({ author: Math.floor(Math.random()*100000) }) which is a big security issue.

  2. It isn't efficient.

    Because updateTask() is generic, it must return task in case a component requires task.modifiedAt — but if some components don't need it, this results in wasted network bandwidth.

This shows how easy it is to introduce security issues and inefficiencies with generic telefunctions.

Generic telefunctions typically:

We recommend the following instead:

// database/actions/task.ts
 
import { getContext } from 'telefunc'
 
// This isn't a telefunction: it's a normal server-side function
export async function updateTask(id: number, mods: Partial<Task>) {
  const { user } = getContext() // Can also be used in normal functions
  const task = await Task.update(mods).where({ id, author: user.id })
  // Returns the updated value task.modifiedAt
  return task
}
// components/TodoList.telefunc.ts
 
import { updateTask } from '../database/actions/task'
 
// Returns task.modifiedAt
export const onTodoTextUpdate = (id: number, text: string) => updateTask(id, { text })
// Doesn't return task.modifiedAt
export const onTodoCompleted = (id: number) => { await updateTask(id, { completed: false }) }

It's slightly less DRY but, in exchange, you get a much clearer structure around security and performance.

When a telefunction is tightly coupled with a component:

  • The telefunction's return value can be minimal (exactly and only what is needed by the component), which leads to increased performance.
  • The telefunction's arguments can be minimal (exactly and only what is needed by the component), which leads to increased security.
  • The telefunction can allow only what is strictly required by the component.

    A cornerstone of security is to grant only the permissions that are strictly required.

That's why we recommend event-based telefunctions, along with the naming convention to ensure telefunctions are tightly coupled to components.

If there are two UI components that could use the exact same telefunction — wouldn't it be nice to create a single telefunction instead of duplicating the same telefunction?

  • It's a rare situation (UI components usually have slightly different requirements).
  • Consider creating a new shared UI component used by these two components.
  • Using the deduplication approach shown above, only one line of duplicated code remains:
    // TodoItem.telefunc.js
    // Defined once for <TodoItem>
    export const onTodoTextUpdate = (id: number, text: string) => updateTask(id, { text })
    // TodoList.telefunc.js
    // Defined again for <TodoList> — the code duplication is only one line of code
    export const onTodoTextUpdate = (id: number, text: string) => updateTask(id, { text })

Naming convention

As explained in the example above, for a clear structure and more effective use of Telefunc, we recommend the following convention.

Name telefunctions onSomeEvent():

    TELEFUNCTIONS
    =============
  updateTodo()
  onTodoTextUpdate()
  onTodoComplete()
 
  loadData()
  onLoad()
  onPagination()
  onInfiniteScroll()

Co-locate .telefunc.js files next to UI component files:

    FILES
    =====
    components/TodoItem.tsx
  components/TodoItem.telefunc.ts
  database/todo.telefunc.ts
 
    components/User.vue
  components/User.telefunc.js
  database/user/getLoggedInUser.telefunc.js

This convention is optional and you can opt-out.

Opt out

Telefunc shows a warning if you don't follow the naming convention — you can opt-out of the convention and remove the warning by setting config.disableNamingConvention.

Opting out of the naming convention is perfectly fine, though we recommend having a clear reason for doing so.

We recommend reading the example above before opting out. It explains why event-based telefunctions lead to increased:

  • Development speed
  • Security
  • Performance

Feel free to reach out if you have questions.

See also