Edit this page

Event-based telefunctions

With REST or GraphQL, API endpoints are:

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

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

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

This inversion leads to significantly faster development.

You may be tempted to create generic telefunctions out of habit from REST or GraphQL, but this is usually an anti-pattern when using Telefunc. Instead, we recommend implementing what we call event-based telefunctions.

In the example below, we explain why it leads to better RPC security and improved RPC speed.

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 }

With REST, it's usually the backend team that is responsible for defining and implementing the API.

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')
}

With Telefunc, it's usually the frontend team that is responsible for defining and implementing telefunctions.

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

Convention

We recommend naming telefunctions onSomeEvent() (see Naming convention), since telefunction calls are always triggered by some kind of event — typically a user action (such as the DOM's click event when the user clicks on a button, or the load event when the user opens the page).

# Co-locating the component's telefunctions
components/TodoList.telefunc.ts
components/TodoList.tsx
// components/TodoList.tsx
// Environment: client
 
import { onMarkAllAsCompleted } from './TodoList.telefunc.ts'
 
function TodoList() {
  return <>
    {/* ... */}
    <button onClick={onMarkAllAsCompleted}>
      Mark all as completed
    </button>
  </>
}

The purpose of the naming convention is to tightly couple telefunctions with components.

With Telefunc, you think in terms of what the frontend needs, instead of thinking in terms of the backend publicly exposing data. From that perspective, it's a lot more natural to co-locate telefunctions next to UI components instead of where the data comes from.

Too restrictive convention?

You might find the naming convention annoyingly restrictive, but it brings important advantages — which we now explain.

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 udpateTask(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 has issues:

  • It makes reasoning about RPC security harder, leading to subtle bugs and security issues.

    As explained at Overview > RPC, telefunctions are public: the user can call updateTask({ author: Math.floor(Math.random()*100000) }) which is obviously an issue.

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

  • It decreases RPC speed.

    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.

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 udpateTask(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 speed.

When a telefunction is tightly coupled with a component:

  • The telefunction's arguments can be minimal (exactly and only what is needed) leading to better security.
  • The telefunction's return value can be minimal (exactly and only what is needed) leading to improved speed.

That's why we recommend event-based telefunctions using the naming convention.

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 two?

  • It's a rare situation (UI components usually have slightly different requirements).
  • Consider creating a new shared UI component used by these two UI components.
  • Using the deduplication approach show 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 crystal-clear structure and more effective use of Telefunc, we recommend the following convention.

Name telefunctions onSomeEvent() (prefix them with on[A-Z]).

    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

Telefunc shows a warning if you don't follow the naming convention — you can remove the warning with config.disableNamingConvention.

Not following the naming convention is perfectly fine, though we recommend having a clear reason for breaking the rule.

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

Feel free to reach out if you have quesions.

Exception: several clients

If your telefunctions are used by multiple clients, it can make sense to define a few generic telefunctions that cover all clients, instead of creating different telefunctions for each client.

Alternatively, you can deploy one Telefunc server per client to preserve the fast development speed of tailored telefunctions.