Skip to content

Commit e13049f

Browse files
blog: Custom Functions in TypeScript
Signed-off-by: David Dal Busco <david.dalbusco@outlook.com>
1 parent 33b6b0f commit e13049f

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

  • blog/2026-03-16-custom-functions-in-typescript
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
---
2+
slug: custom-functions-in-typescript
3+
title: Custom Functions in TypeScript
4+
authors: [peterpeterparker]
5+
tags: [functions, typescript, sputnik, serverless]
6+
date: 2026-03-16
7+
---
8+
9+
For many use cases, frontend code gets you far. But even in smaller projects, you quickly hit a point where you need some backend logic. That's where serverless functions come in.
10+
11+
The idea behind Juno's functions is that you should be able to write them in either Rust or TypeScript. Hooks have been available in TypeScript for a while and are handy for reacting to events. But developers also often want to define their own functions, query or update, akin to adding custom endpoints to an HTTP API. That wasn't supported in TypeScript, until now 🚀.
12+
13+
---
14+
15+
## Custom Functions
16+
17+
Custom functions let you define callable endpoints directly inside your Satellite, which can be explicitly invoked from your frontend or from other services.
18+
19+
There are two kinds.
20+
21+
A **query** is read-only. It returns data without touching any state, and it's fast. Use it when you just need to fetch something.
22+
23+
An **update** can read and write. Use it when your logic needs to persist data, trigger side effects, or when you need the response to be certified, making it suitable for security-sensitive use cases where data integrity must be guaranteed.
24+
25+
```typescript
26+
import { defineQuery } from "@junobuild/functions";
27+
28+
export const ping = defineQuery({
29+
handler: () => "pong"
30+
});
31+
```
32+
33+
The functions can be described with optional arguments and results. They are strongly typed both at runtime and at build time thanks to a new type system, and their handlers can be synchronous or asynchronous.
34+
35+
```typescript
36+
import { defineUpdate } from "@junobuild/functions";
37+
import { j } from "@junobuild/schema";
38+
39+
const ArgsSchema = j.strictObject({
40+
name: j.string()
41+
});
42+
43+
const ResultSchema = j.strictObject({
44+
message: j.string()
45+
});
46+
47+
export const myUpdate = defineUpdate({
48+
args: ArgsSchema,
49+
returns: ResultSchema,
50+
handler: async ({ name }) => {
51+
// your logic here
52+
return { message: `Saved ${name}.` };
53+
}
54+
});
55+
```
56+
57+
---
58+
59+
## Auto-generated Bindings
60+
61+
Another part that makes this genuinely fun to use: when you build your project, a type-safe client API is automatically generated based on your function definitions. No glue code, no manual wiring, no thinking about serialization. Your functions are simply available through the `functions` namespace.
62+
63+
```typescript
64+
import { functions } from "../declarations/satellite/satellite.api.ts";
65+
66+
await functions.ping();
67+
await functions.myUpdate({ name: "David" });
68+
```
69+
70+
Define the shape on the backend, call it from the frontend with full type safety. That's it.
71+
72+
---
73+
74+
## Schema Types
75+
76+
Arguments and return types are optional, but when you need them, Juno provides a type system built on top of [Zod](https://zod.dev) to let you define their shapes.
77+
78+
```typescript
79+
import { j } from "@junobuild/schema";
80+
```
81+
82+
Those schemas are validated at runtime and used at build time to generate all the necessary bindings. You define the shape once, you get safety everywhere.
83+
84+
```typescript
85+
const Schema = j.strictObject({
86+
name: j.string(),
87+
age: j.number()
88+
});
89+
```
90+
91+
Since you will likely need some environment-specific types, `j` extends Zod with `j.principal()` and `j.uint8array()`.
92+
93+
```typescript
94+
const Schema = j.strictObject({
95+
owner: j.principal(),
96+
data: j.uint8array()
97+
});
98+
```
99+
100+
And when your function needs to handle multiple distinct input shapes, reach for `discriminatedUnion`:
101+
102+
```typescript
103+
import { defineUpdate } from "@junobuild/functions";
104+
import { j } from "@junobuild/schema";
105+
106+
const Schema = j.discriminatedUnion("type", [
107+
j.strictObject({ type: j.literal("cat"), indoor: j.boolean() }),
108+
j.strictObject({ type: j.literal("dog"), breed: j.string() })
109+
]);
110+
111+
export const registerPet = defineUpdate({
112+
args: Schema,
113+
handler: ({ args }) => {
114+
if (args.type === "cat") {
115+
// handle cat
116+
} else {
117+
// handle dog
118+
}
119+
}
120+
});
121+
```
122+
123+
Long story short, `j` is Zod with a few extras and everything you need to strongly type your functions.
124+
125+
---
126+
127+
## References
128+
129+
Following sections of the documentation have been updated:
130+
131+
- [Development](/docs/build/functions/development/)
132+
- [Guides](/docs/guides/typescript)
133+
- [References (SDK, schema, etc.)](/docs/reference/functions/typescript)
134+
135+
---
136+
137+
I'm genuinely excited about these improvements and can totally see myself not using Rust for writing serverless functions in most of my projects in a near future. Not entirely there yet, HTTPS outcalls support in TypeScript is still coming, but getting there.
138+
139+
To infinity and beyond<br />David

0 commit comments

Comments
 (0)