Telefunc enables functions defined on the server-side to be called remotely from the browser-side.
// hello.telefunc.js// Environment: serverexport { hello }// hello() always runs on the server-sideasync function hello({ name }) { const message = 'Welcome ' + name return { message }}
<!-- index.html --><!-- Environment: client --><html> <body> <script type="module"> // This import doesn't actually load the hello.telefunc.js file: Telefunc transforms // hello.telefunc.js into a thin HTTP client. import { hello } from './hello.telefunc.js' // This thin HTTP client makes an HTTP request when the hello() function is called. const { message } = await hello({ name: 'Eva' }) console.log(message) // Prints 'Welcome Eva' </script> </body></html>
The central aspect here is that hello() is always executed on the server side, which enables hello() to use server-side utilities such as SQL and ORMs.
ORM & SQL
As we have seen in the previous section, telefunctions always run on the server-side and can therefore use server utilities such as SQL and ORMs.
.telefunc.js files are guaranteed to be loaded only on the server-side. You can therefore save secret information, such as the database passwords, in .telefunc.js files.
// TodoList.jsx// Environment: client// This doesn't actually load CreateTodo.telefunc.js which we'll explain in the next sectionimport { onLoad } from './TodoList.telefunc.js'async function TodoList() { // The frontend uses the telefunction onLoad() to retrieve data by executing a SQL/ORM query const todoItems = await onLoad() return ( <ul>{ todoItems.map(item => <li>{ item.text }</li> )}</ul> )}
While the examples here use JSX, Telefunc works with any UI framework (React, Vue, Svelte, Solid, ...).
Telefunctions can also be used to mutate data:
// CreateTodo.telefunc.js// Environment: serverexport { onNewTodo }import { shield } from 'telefunc'// We'll talk about shield() latershield(onNewTodo, [shield.type.string])async function onNewTodo(text) { // ORM const todoItem = new Task({ text }) await todoItem.save() // SQL await execute( "INSERT INTO todo_items VALUES (:text)", { text } )}
// CreateTodo.jsx// Environment: clientimport { onNewTodo } from './CreateTodo.telefunc.js'async function onClick(form) { const text = form.input.value await onNewTodo(text)}function CreateTodo() { return ( <form> <input input="text"></input> <button onClick={onClick}>Add To-Do</button> </form> )}
RPC enables your frontend to tap directly into the full power of the server such as SQL and ORMs. For most use cases it's simpler, more flexible, and more performant than REST and GraphQL.
GraphQL and RESTful can be better than RPC if:
you want to give third parties generic access to your data, or
you are a very large company with highly complex databases.
// sql.telefunc.js// Environment: server// run() is public: it can be called by anyoneexport { run }async function run(sql) { return await database.execute(sql)}
This run() telefunction essentially exposes the entire database to the world as
anyone can make this HTTP request:
As we've seen in the previous section,
the following telefunction isn't safe.
// run.telefunc.js// Environment: server// run() is public: it can be called by anyoneexport { run }async function run(sql) { return await database.execute(sql)}
But we can use throw Abort() to protect it:
// run.telefunc.js// Environment: server// run() is public: it can be called by anyoneexport { run }import { Abort, getContext } from 'telefunc'async function run(sql) { const { user } = getContext() // Only admins are allowed to run this telefunction if (user.isAdmin !== true) throw Abort() return await database.execute(sql)}
Telefunctions can access contextual information by using getContext().
We can use throw Abort() to avoid any forbidden telefunction call.
// TodoList.telefunc.js// Environment: serverexport { onLoad }import { Abort, getContext } from 'telefunc'async function onLoad() { const { user } = getContext() // We forbid onLoad() to be called by a user that isn't logged-in if (!user) throw Abort() const todoList = await Task.findMany({ authorId: user.id }) return todoList}
We essentially use throw Abort() to implement permission: only a logged-in user is allowed to fetch its to-do items.
We talk more about permissions at Guides > Permissions.
In principle, we could also throw new Error() instead of throw Abort() as it also interupts the telefunction call. But we recommend throw Abort() as it comes with many conveniences.
If, upon aborting a telefunction call, you want to pass information to the frontend then use return someValue or throw Abort(someValue), see Guides > Permissions.
shield()
Since telefunctions are public and can be called by anyone, we cannot assuming anything about arguments. We can use throw Abort() to ensure the type of telefunction arguments:
// CreateTodo.telefunc.js// Environment: serverexport async function onNewTodo(text) { // ❌ This may throw: // ``` // Uncaught TypeError: Cannot read properties of undefined (reading 'toUpperCase') // ``` // While the frontend may always call onNewTodo(text) with `typeof text === 'string'`, // because onNewTodo() is public, anyone can call onNewTodo(undefined) instead. text = text.toUpperCase() // ✅ We ensure `text` is a string if (typeof text !== 'string') { throw Abort() } text = text.toUpperCase()}
For more convenience, we can use shield() instead:
// CreateTodo.telefunc.js// Environment: serverimport { shield } from 'telefunc'const t = shield.typeshield(onNewTodo, [t.string])export async function onNewTodo(text) { // ...}
// CreateTodo.telefunc.js// Environment: serverimport { shield } from 'telefunc'const t = shield.typeshield(onNewTodo, [{ text: t.string, isCompleted: t.boolean }])export async function onNewTodo({ text, isCompleted }) { // ...}
Not only does shield() call throw Abort() on our behalf, but it also infers the type of the arguments for TypeScript and IntelliSense.
Any of your telefunctions can be called by anyone, at any time, and with any arguments. One way to think about it is that any random telefunction call can happen at any time.
You should always protect your telefunctions, even when your frontend calls a telefunction only in a certain way. For example:
// Comment.jsx// Environment: clientimport { onDelete } from './Comment.telefunc.js'function Comment({ id, text }) { const deleteButton = // The delete button is only shown to admins !user.isAdmin ? null : <button onClick={() => onDelete(id)}>Delete</button> return <> <p>{ text }</p> { deleteButton } </>}
Because the frontend shows the delete button only to admins, we can assume the user to be an admin whenever the frontend calls onDelete().
But we still need to use throw Abort() in order to protect the telefunction against calls that don't originate from the frontend.
// Comment.telefunc.js// Environment: serverimport { getContext, Abort, shield } from 'telefunc'shield(onDelete, [shield.type.number])export async function onDelete(id) { const { user } = getContext() // onDelete() is public and anyone can call it without being an admin. // We must abort if that happens. if (!user.isAdmin) throw Abort() // ...}
RPC speed
When used correctly, RPC is highly efficient.
Since telefunctions are tailored to your frontend, you can make them send only the strict minimum amount of data.
For example, the following doesn't send a single byte of superfluous information:
// TodoList.telefunc.ts// Environment: serverimport { getContext } from 'telefunc'export async function onTodoComplete(id: number) { const { user } = getContext() await Task.update({ completed: true }).where({ id, author: user.id }) // No data is returned: the HTTP response status code 200 is enough information for the client}
With REST, because API endpoints are generic you often end up returning superfluous data.
Even with GraphQL, the client must specify and send the list of fields that the server needs to return. This isn't needed with RPC — that information is implicity defined by the telefunction name (i.e. the information is compressed).
For efficient telefunctions, we recommend creating tailored telefunctions instead of generic ones — see Guides > Event-based telefunctions.
Telefunc newcomers often make the mistake of creating generic telefunctions — a habit that comes from using REST or GraphQL.