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 tasksGET https://api.todo.com/task?completed=false ∅# Make a request per task to update itPOST 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: serverimport { 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 telefunctionscomponents/TodoList.telefunc.tscomponents/TodoList.tsx
// components/TodoList.tsx// Environment: clientimport { 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: serverimport { Task } from '../models/Task'import { getContext } from 'telefunc'// One telefunction used by multiple UI componentsexport 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.
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.tsimport { getContext } from 'telefunc'// This isn't a telefunction: it's a normal server-side functionexport 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}
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 codeexport 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]).
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.