Skip to content

Commit 4f98cf0

Browse files
committed
feat: add documentation
1 parent 2673021 commit 4f98cf0

File tree

15 files changed

+314
-58
lines changed

15 files changed

+314
-58
lines changed

LICENSE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2025 [Falk33n](https://github.com/Falk33n)
4+
Copyright (c) 2025 [nilspettersson](https://github.com/nilspettersson)
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

README.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# SvelteKit RPC
2+
3+
This is a simple and type-safe [Remote Procedure Call (`RPC`)](https://www.techtarget.com/searchapparchitecture/definition/Remote-Procedure-Call-RPC) implementation in a [`SvelteKit`](https://svelte.dev/docs/kit/introduction) project.
4+
It demonstrates how to call server-side functions _directly_ and _typesafely_ from the client using a minimal setup.
5+
6+
---
7+
8+
## 📚 Table of Contents
9+
10+
- [🧠 How It Works](#-how-it-works)
11+
- [🚀 Getting Started](#-getting-started)
12+
- [1. Clone the repository](#1-clone-the-repository)
13+
- [2. Install dependencies](#2-install-dependencies)
14+
- [3. Start the dev server](#3-start-the-dev-server)
15+
- [🛠️ How To Use](#-how-to-use)
16+
- [1. Define the routes](#1-define-the-routes)
17+
- [2. Create the app router](#2-create-the-app-router)
18+
- [3. Calling the API](#3-calling-the-api)
19+
- [4. Creating an middleware](#4-creating-an-middleware)
20+
- [📃 License](#license)
21+
- [🐛 Found a Bug?](#-found-a-bug)
22+
23+
---
24+
25+
## 🧠 How It Works
26+
27+
Server-side functions are defined in [`$lib/server/routers/*.ts`](./src/lib/server/routers) and combined inside the [`appRouter`](./src/lib/server/root.ts) in [`$lib/server/root.ts`](./src/lib/server/root.ts).
28+
29+
The [`/api/[...endpoints]`](./src/routes/api/[...endpoints]/+server.ts) route catches [`RPC`](https://www.techtarget.com/searchapparchitecture/definition/Remote-Procedure-Call-RPC) requests and runs the matching function using the [`callRouter`](./src/lib/server/caller.ts) function in [`$lib/server/caller.ts`](./src/lib/server/caller.ts).
30+
31+
The client makes a fetch call to that route using the [`callApi`](./src/lib/client/caller.ts) function in [`$lib/client/caller.ts`](./src/lib/client/caller.ts). Which provides full type safety and [`IntelliSense`](https://code.visualstudio.com/docs/editing/intellisense) for the defined route.
32+
33+
[`Zod`](https://zod.dev/?id=introduction) is used to make validations of the input before the actual function runs but also validates the output before it reaches its end destination. This functionality is wrapped inside the base middleware [`publicProcedure`](./src/lib/server/setup.ts) in [`$lib/server/setup.ts`](./src/lib/server/setup.ts).
34+
35+
[`TypeScript`](https://www.typescriptlang.org/docs/) ensures type safety for arguments and return values but also [`IntelliSense`](https://code.visualstudio.com/docs/editing/intellisense) for the developer.
36+
37+
## 🚀 Getting Started
38+
39+
### 1. Clone the repository
40+
41+
```bash
42+
git clone https://github.com/Falk33n/sveltekit-rpc.git
43+
cd sveltekit-rpc
44+
```
45+
46+
### 2. Install dependencies
47+
48+
```bash
49+
# (or use `bun install` / `pnpm install` / `yarn install` if you prefer).
50+
npm install
51+
```
52+
53+
### 3. Start the dev server
54+
55+
```bash
56+
# (or use `bun run dev` / `pnpm run dev` / `yarn run dev` if you prefer).
57+
npm run dev
58+
```
59+
60+
Your app should now be running at [http://localhost:5173](http://localhost:5173).
61+
62+
## 🛠️ How To Use
63+
64+
### 1. Define the routes
65+
66+
```ts
67+
import { exampleInputSchema, exampleOutputSchema } from '$lib/schemas/example';
68+
import { createRouter, publicProcedure } from '../setup';
69+
70+
export const exampleRouter = createRouter({
71+
example: {
72+
method: 'post',
73+
handler: publicProcedure
74+
.input(exampleInputSchema)
75+
.output(exampleOutputSchema)
76+
.resolve(async (event, input) => {
77+
// The input is directly parsed from the input schema.
78+
const { id } = input;
79+
80+
// The event is the same as the event that comes from SvelteKit backend.
81+
console.log(event.params);
82+
83+
// The output is directly parsed from the output schema.
84+
return {
85+
status: 200,
86+
data: { id, name: 'hanna' },
87+
message: 'OK',
88+
};
89+
}),
90+
},
91+
});
92+
```
93+
94+
### 2. Create the app router
95+
96+
```ts
97+
import { exampleRouter } from './routers';
98+
import { createAppRouter } from './setup';
99+
100+
// This is the root of all the endpoints, we define this to get a better structure of different
101+
// categories of routers.
102+
export const appRouter = createAppRouter({
103+
example: exampleRouter,
104+
});
105+
```
106+
107+
### 3. Calling the API
108+
109+
```svelte
110+
<script lang="ts">
111+
import { callApi } from '../lib/client/caller';
112+
113+
async function onclick() {
114+
// Fully typesafe api caller, that will give you the correct input,
115+
// output and method based on the endpoint string.
116+
const api = await callApi('example.example', {
117+
input: { id: 'hoho' },
118+
method: 'post',
119+
});
120+
121+
// Logs the return object of the api.
122+
console.log(api);
123+
}
124+
</script>
125+
126+
<button {onclick}>test</button>
127+
```
128+
129+
### 4. Creating an middleware
130+
131+
```ts
132+
import { publicProcedure } from '../setup';
133+
134+
// This is how we define an middleware to be used and called inbefore each endpoint runs.
135+
const exampleProcedure = publicProcedure.use((event) => console.log(event.cookies.getAll()));
136+
```
137+
138+
## License
139+
140+
Distributed under the [`MIT` License](https://memgraph.com/blog/what-is-mit-license). This project is open source and free to use, modify, and distribute under the terms of the [`MIT` License](https://memgraph.com/blog/what-is-mit-license).
141+
142+
You can find the full license text in the [`LICENSE.md`](./LICENSE.md) file.
143+
144+
## 🐛 Found a Bug?
145+
146+
Open an issue or create a pull request. Contributions are always welcome!
147+
148+
If you discover a **security vulnerability**, please **do not** open an issue or create a pull request.
149+
Instead, report it **privately** by emailing [**tim.falk00@gmail.com**](mailto:tim.falk00@gmail.com) _or_ [**nils-pettsson@outlook.com**](mailto:nils-pettsson@outlook.com). Thank you for being responsible!

bun.lockb

16 KB
Binary file not shown.

package.json

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "api-typesafety-playground",
2+
"name": "sveltekit-rpc",
33
"private": true,
4-
"version": "0.0.1",
4+
"version": "1.0.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite dev",
@@ -15,31 +15,31 @@
1515
},
1616
"devDependencies": {
1717
"@eslint/compat": "^1.2.8",
18-
"@eslint/js": "^9.23.0",
18+
"@eslint/js": "^9.24.0",
1919
"@sveltejs/adapter-auto": "^6.0.0",
20-
"@sveltejs/kit": "^2.20.3",
20+
"@sveltejs/kit": "^2.20.7",
2121
"@sveltejs/vite-plugin-svelte": "^5.0.3",
22-
"@tailwindcss/vite": "^4.1.2",
23-
"bits-ui": "^1.3.16",
22+
"@tailwindcss/vite": "^4.1.4",
23+
"bits-ui": "^1.3.19",
2424
"clsx": "^2.1.1",
25-
"eslint": "^9.23.0",
26-
"eslint-config-prettier": "^10.1.1",
25+
"eslint": "^9.24.0",
26+
"eslint-config-prettier": "^10.1.2",
2727
"eslint-plugin-svelte": "^3.5.1",
2828
"globals": "^16.0.0",
29-
"mode-watcher": "^0.5.1",
29+
"mode-watcher": "^1.0.1",
3030
"prettier": "^3.5.3",
3131
"prettier-plugin-svelte": "^3.3.3",
3232
"prettier-plugin-tailwindcss": "^0.6.11",
33-
"svelte": "^5.25.6",
34-
"svelte-check": "^4.1.5",
33+
"svelte": "^5.27.0",
34+
"svelte-check": "^4.1.6",
3535
"svelte-sonner": "^0.3.28",
36-
"tailwind-merge": "^3.1.0",
36+
"tailwind-merge": "^3.2.0",
3737
"tailwind-variants": "^1.0.0",
38-
"tailwindcss": "^4.1.2",
38+
"tailwindcss": "^4.1.4",
3939
"tw-animate-css": "^1.2.5",
40-
"typescript": "^5.8.2",
41-
"typescript-eslint": "^8.29.0",
42-
"vite": "^6.2.5",
43-
"zod": "^3.24.2"
40+
"typescript": "^5.8.3",
41+
"typescript-eslint": "^8.30.1",
42+
"vite": "^6.3.1",
43+
"zod": "^3.24.3"
4444
}
4545
}

src/app.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<link
6-
rel="icon"
7-
href="%sveltekit.assets%/favicon.png"
8-
/>
95
<meta
106
name="viewport"
117
content="width=device-width, initial-scale=1"

src/lib/client/caller.ts

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { toast } from 'svelte-sonner';
22
import type { z } from 'zod';
33
import type { routerBaseOutputSchema } from '../schemas';
4-
import type { AppRouterEndpoints } from '../server/caller';
5-
import type { AppRouter } from '../server/root';
6-
import type { HttpMethod } from '../server/setup';
4+
import type { AppRouter, HttpMethod } from '../server/setup';
75

6+
/** Resolves a nested path string (e.g., "users.get") to the corresponding type within a nested object. */
87
type ResolvePath<T, P extends string> = P extends `${infer K}.${infer Rest}`
98
? K extends keyof T
109
? ResolvePath<T[K], Rest>
@@ -13,27 +12,65 @@ type ResolvePath<T, P extends string> = P extends `${infer K}.${infer Rest}`
1312
? T[P]
1413
: never;
1514

16-
export type ExtractMethod<T, P extends string> =
17-
ResolvePath<T, P> extends { method: infer M } ? M : never;
15+
/** Extracts the `HTTP` method from a router endpoint. */
16+
type ExtractMethod<T, P extends string> = ResolvePath<T, P> extends { method: infer M } ? M : never;
1817

19-
export type ExtractInput<T, P extends string> =
20-
ResolvePath<T, P> extends { handler: (...args: infer Params) => any } ? Params[1] : never;
18+
/** Extracts the `input` from a router endpoint. */
19+
type ExtractInput<T, P extends string> =
20+
ResolvePath<T, P> extends { handler: (...args: infer Params) => unknown } ? Params[1] : never;
2121

22-
export type ExtractOutput<T, P extends string> =
23-
ResolvePath<T, P> extends { handler: (...args: any) => infer R } ? R : never;
22+
/** Extracts the `output` from a router endpoint. */
23+
type ExtractOutput<T, P extends string> =
24+
ResolvePath<T, P> extends { handler: (...args: unknown[]) => infer R } ? R : never;
2425

26+
/**
27+
* Optional request configuration excluding the body and method,
28+
* which are controlled internally in the `callApi` function.
29+
*/
2530
type RequestOptions = Omit<RequestInfo, 'body' | 'method'>;
2631

32+
/** Input arguments for calling an API endpoint through the `callApi` function. */
2733
type ClientCallerInput<T extends AppRouterEndpoints> = {
34+
/**
35+
* The HTTP method to use for the request (e.g., "GET", "POST", "PATCH").
36+
* Inferred from the backend endpoint's `method` property.
37+
*/
2838
method: ExtractMethod<AppRouter, T>;
39+
40+
/**
41+
* The payload/body to send with the request.
42+
* Inferred from the second parameter of the backend endpoint's `handler` function.
43+
*/
2944
input: ExtractInput<AppRouter, T>;
45+
46+
/**
47+
* Optional configuration for the fetch request, excluding `body` and `method` which are handled internally.
48+
*/
3049
requestOptions?: RequestOptions;
3150
};
3251

52+
/**
53+
* Output type of the `callApi` function. It either returns the defined endpoint output,
54+
* a base error schema, or undefined if the request fails unexpectedly.
55+
*/
3356
type ClientCallerOutput<T extends AppRouterEndpoints> = Promise<
3457
ExtractOutput<AppRouter, T> | z.infer<typeof routerBaseOutputSchema> | undefined
3558
>;
3659

60+
/** Flattens the `router object` to become a string containing each `endpoint` path seperated by a `"."` */
61+
type FlattenRouter<T> = {
62+
[K in Extract<keyof T, string | number>]: T[K] extends Record<string, unknown>
63+
? `${K}.${Extract<keyof T[K], string | number>}`
64+
: K;
65+
}[Extract<keyof T, string | number>];
66+
67+
/** All the defined flattened `AppRouter` endpoints. */
68+
type AppRouterEndpoints = FlattenRouter<AppRouter>;
69+
70+
/**
71+
* Function responsible for calling the backend router endpoints from the client.
72+
* This is the only client function that has a relationship to the backend.
73+
*/
3774
export async function callApi<T extends AppRouterEndpoints>(
3875
endpoint: T,
3976
{ method, input, requestOptions }: ClientCallerInput<T>,
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
import Root from './sonner.svelte';
2-
3-
export { Root, Root as Toaster };
1+
export { default as Toaster } from './sonner.svelte';

src/lib/schemas/base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22

3+
/** The base output schema of each endpoint. */
34
export const routerBaseOutputSchema = z.object({
45
status: z
56
.number({

src/lib/server/caller.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { error, json } from '@sveltejs/kit';
22
import type { z } from 'zod';
33
import type { routerBaseOutputSchema } from '../schemas';
4-
import { appRouter, type AppRouter } from './root';
4+
import { appRouter } from './root';
55
import type { ProcedureFunction, RouteEvent } from './setup';
66

7-
function isValidEndpoint(router: any, endpoint: string): boolean {
7+
/** Function that validates if the given `endpoint` exists in the given `router`. */
8+
function isValidEndpoint(router: unknown, endpoint: string) {
89
const parts = endpoint.split('/');
10+
11+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
912
let current: any = router;
1013

1114
for (let i = 0; i < parts.length - 1; i++) {
@@ -16,14 +19,10 @@ function isValidEndpoint(router: any, endpoint: string): boolean {
1619
return parts[parts.length - 1] in current;
1720
}
1821

19-
type FlattenRouter<T> = {
20-
[K in Extract<keyof T, string | number>]: T[K] extends Record<string, any>
21-
? `${K}.${Extract<keyof T[K], string | number>}`
22-
: K;
23-
}[Extract<keyof T, string | number>];
24-
25-
export type AppRouterEndpoints = FlattenRouter<AppRouter>;
26-
22+
/**
23+
* Function that executes the correct endpoint from the given `event.params.endpoints` slug.
24+
* This is the function that gets called in each `SvelteKit` `+server.ts` endpoint.
25+
*/
2726
export async function callRouter(event: RouteEvent) {
2827
const endpoints = event.params.endpoints;
2928

@@ -35,6 +34,7 @@ export async function callRouter(event: RouteEvent) {
3534

3635
const input = (await event.request.json()) as unknown;
3736

37+
// Calls the correct endpoint of the appRouter.
3838
const response = await (
3939
appRouter[route as never][endpoint]['handler'] as ProcedureFunction<
4040
typeof input,

src/lib/server/root.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { exampleRouter } from './routers';
22
import { createAppRouter } from './setup';
33

4+
/**
5+
* The root router of the application. This is where you define and structure
6+
* all your categorized routers.
7+
*/
48
export const appRouter = createAppRouter({
59
example: exampleRouter,
610
});
7-
8-
export type AppRouter = typeof appRouter;

0 commit comments

Comments
 (0)