vite-plugin-server-actions 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,192 +1,685 @@
1
- # 🚀 Vite Server Actions
1
+ # Vite Server Actions
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/vite-plugin-server-actions.svg?style=flat)](https://www.npmjs.com/package/vite-plugin-server-actions)
4
4
  [![Downloads](https://img.shields.io/npm/dm/vite-plugin-server-actions.svg?style=flat)](https://www.npmjs.com/package/vite-plugin-server-actions)
5
- [![Build Status](https://img.shields.io/github/workflow/status/HelgeSverre/vite-plugin-server-actions/CI)](https://github.com/HelgeSverre/vite-plugin-server-actions/actions)
6
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
6
 
8
- > 🚧 **Experimental:** This is currently a proof of concept. Use at your own risk.
7
+ **Write server functions. Call them from the client. That's it.**
9
8
 
9
+ Vite Server Actions brings the simplicity of server-side development to your Vite applications. Import server functions into your client code and call them directly - no API routes, no HTTP handling, no boilerplate.
10
10
 
11
- **Vite Server Actions** is a Vite plugin that makes it easy to create functions (actions) that runs on the server, while
12
- allowing you to call them from the client-side as if they were local functions.
11
+ ```javascript
12
+ // server/db.server.js
13
+ export async function getUsers() {
14
+ return await database.users.findAll();
15
+ }
16
+
17
+ // App.vue
18
+ import { getUsers } from "./server/db.server.js";
19
+
20
+ const users = await getUsers(); // Just call it!
21
+ ```
22
+
23
+ ## 🚀 Why Vite Server Actions?
24
+
25
+ - **Zero API Boilerplate** - No need to define routes, handle HTTP methods, or parse request bodies
26
+ - **Type Safety** - Full TypeScript support with proper type inference across client-server boundary
27
+ - **Built-in Validation** - Automatic request validation using Zod schemas
28
+ - **Auto Documentation** - OpenAPI spec and Swagger UI generated from your code
29
+ - **Production Ready** - Builds to a standard Node.js Express server
30
+ - **Developer Experience** - Hot reload, middleware support, and helpful error messages
13
31
 
14
- ## ✨ Features
32
+ ## ✨ Core Features
15
33
 
16
- - 🔄 Automatic API endpoint creation for server functions (e.g. `POST /api/todos/addTodo`)
17
- - 🔗 Seamless client-side proxies for easy usage (e.g. `import {addTodo} from './server/todos.server.js'`)
18
- - 🛠 Support for both development and production environments ( `vite build` )
19
- - 🚀 Zero-config setup for instant productivity
34
+ - 🔗 **Seamless Imports** - Import server functions like any other module
35
+ - 🛡️ **Secure by Default** - Server code never exposed to client
36
+ - **Request Validation** - Attach Zod schemas for automatic validation
37
+ - 📖 **API Documentation** - Auto-generated OpenAPI specs and Swagger UI
38
+ - 🔌 **Middleware Support** - Add authentication, logging, CORS, etc.
39
+ - 🎯 **Flexible Routing** - Customize how file paths map to API endpoints
40
+ - 📦 **Production Optimized** - Builds to efficient Express server with all features
20
41
 
21
42
  ## 🚀 Quick Start
22
43
 
23
- 1. Install the plugin:
44
+ ### 1. Install
24
45
 
25
46
  ```bash
26
- # Install using npm
27
47
  npm install vite-plugin-server-actions
28
-
29
- # Install using yarn
30
- yarn add vite-plugin-server-actions
31
48
  ```
32
49
 
33
- 2. Add to your `vite.config.js`:
50
+ ### 2. Configure Vite
34
51
 
35
52
  ```javascript
36
- import {defineConfig} from "vite";
37
- import serverActions from "helgesverre/vite-plugin-server-actions";
53
+ // vite.config.js
54
+ import { defineConfig } from "vite";
55
+ import serverActions from "vite-plugin-server-actions";
38
56
 
39
57
  export default defineConfig({
40
- plugins: [serverActions()],
58
+ plugins: [
59
+ serverActions(), // That's it! Zero config needed
60
+ ],
41
61
  });
42
62
  ```
43
63
 
44
- 3. Create a `[whatever].server.js` file anywhere in your project:
64
+ ### 3. Create a Server Function
65
+
66
+ Any file ending with `.server.js` becomes a server module:
45
67
 
46
68
  ```javascript
47
- // ex: src/actions/todo.server.js
48
- import fs from "fs";
49
- import path from "path";
69
+ // actions/todos.server.js
70
+ import { db } from "./database";
50
71
 
51
- const TODO_FILE_PATH = path.join(process.cwd(), "list-of-todos.json");
72
+ export async function getTodos(userId) {
73
+ // This runs on the server with full Node.js access
74
+ return await db.todos.findMany({ where: { userId } });
75
+ }
52
76
 
53
- export async function deleteTodoById(id) {
54
- const data = fs.readFileSync(TODO_FILE_PATH, "utf-8");
55
- const todos = JSON.parse(data);
56
- const newTodos = todos.filter((todo) => todo.id !== id);
57
- fs.writeFileSync(TODO_FILE_PATH, JSON.stringify(newTodos, null, 2));
77
+ export async function addTodo(text, userId) {
78
+ return await db.todos.create({
79
+ data: { text, userId, completed: false },
80
+ });
58
81
  }
82
+ ```
83
+
84
+ ### 4. Use in Your Client
59
85
 
60
- export async function saveTodoToJsonFile(todo) {
61
- const data = fs.readFileSync(TODO_FILE_PATH, "utf-8");
62
- const todos = JSON.parse(data);
63
- todos.push(todo);
64
- fs.writeFileSync(TODO_FILE_PATH, JSON.stringify(todos, null, 2));
86
+ ```javascript
87
+ // App.jsx
88
+ import { getTodos, addTodo } from './actions/todos.server.js'
89
+
90
+ function TodoApp({ userId }) {
91
+ const [todos, setTodos] = useState([])
92
+
93
+ useEffect(() => {
94
+ // Just call the server function!
95
+ getTodos(userId).then(setTodos)
96
+ }, [userId])
97
+
98
+ async function handleAdd(text) {
99
+ const newTodo = await addTodo(text, userId)
100
+ setTodos([...todos, newTodo])
101
+ }
102
+
103
+ return (
104
+ // Your UI here...
105
+ )
65
106
  }
107
+ ```
108
+
109
+ That's it! The plugin automatically:
110
+
111
+ - ✅ Creates API endpoints for each function
112
+ - ✅ Handles serialization/deserialization
113
+ - ✅ Provides full TypeScript support
114
+ - ✅ Works in development and production
115
+
116
+ ## 📚 Real-World Examples
117
+
118
+ ### Database Operations
119
+
120
+ ```javascript
121
+ // server/database.server.js
122
+ import { PrismaClient } from "@prisma/client";
123
+
124
+ const prisma = new PrismaClient();
66
125
 
67
- export async function listTodos() {
68
- const data = fs.readFileSync(TODO_FILE_PATH, "utf-8");
69
- return JSON.parse(data);
126
+ export async function getUser(id) {
127
+ return await prisma.user.findUnique({
128
+ where: { id },
129
+ include: { profile: true },
130
+ });
131
+ }
132
+
133
+ export async function updateUser(id, data) {
134
+ return await prisma.user.update({
135
+ where: { id },
136
+ data,
137
+ });
70
138
  }
71
139
  ```
72
140
 
73
- 4. Import and use your server actions in your client-side code:
141
+ ### File Uploads
74
142
 
75
- ```svelte
76
- <!-- ex: src/App.svelte -->
77
- <script>
78
- import { deleteTodoById, listTodos, saveTodoToJsonFile } from "./actions/todo.server.js";
143
+ ```javascript
144
+ // server/upload.server.js
145
+ import { writeFile } from "fs/promises";
146
+ import path from "path";
79
147
 
80
- let todos = [];
81
- let newTodoText = "";
148
+ export async function uploadFile(filename, base64Data) {
149
+ const buffer = Buffer.from(base64Data, "base64");
150
+ const filepath = path.join(process.cwd(), "uploads", filename);
82
151
 
83
- async function fetchTodos() {
84
- todos = await listTodos();
85
- }
152
+ await writeFile(filepath, buffer);
153
+ return { success: true, path: `/uploads/${filename}` };
154
+ }
155
+ ```
86
156
 
87
- async function addTodo() {
88
- await saveTodoToJsonFile({ id: Math.random(), text: newTodoText });
89
- newTodoText = "";
90
- await fetchTodos();
91
- }
157
+ ### External API Integration
158
+
159
+ ```javascript
160
+ // server/weather.server.js
161
+ export async function getWeather(city) {
162
+ const response = await fetch(`https://api.weather.com/v1/current?city=${city}&key=${process.env.API_KEY}`);
163
+ return response.json();
164
+ }
165
+ ```
166
+
167
+ ### With Validation
92
168
 
93
- async function removeTodo(id) {
94
- await deleteTodoById(id);
95
- await fetchTodos();
169
+ ```javascript
170
+ // server/auth.server.js
171
+ import { z } from "zod";
172
+ import bcrypt from "bcrypt";
173
+ import { signJWT } from "./jwt";
174
+
175
+ const LoginSchema = z.object({
176
+ email: z.string().email(),
177
+ password: z.string().min(8),
178
+ });
179
+
180
+ export async function login(credentials) {
181
+ // Validation happens automatically!
182
+ const user = await db.users.findByEmail(credentials.email);
183
+
184
+ if (!user || !(await bcrypt.compare(credentials.password, user.passwordHash))) {
185
+ throw new Error("Invalid credentials");
96
186
  }
97
187
 
98
- fetchTodos();
99
- </script>
188
+ return { token: signJWT(user), user };
189
+ }
190
+
191
+ // Attach schema for automatic validation
192
+ login.schema = LoginSchema;
193
+ ```
100
194
 
101
- <div>
102
- <h1>Todos</h1>
103
- <ul>
104
- {#each todos as todo}
105
- <li>
106
- {todo.text}
107
- <button on:click="{() => removeTodo(todo.id)}">Remove</button>
108
- </li>
109
- {/each}
110
- </ul>
111
- <input type="text" bind:value="{newTodoText}" />
112
- <button on:click="{addTodo}">Add Todo</button>
113
- </div>
195
+ ### Complete Examples
196
+
197
+ - [Todo App with Svelte](examples/svelte-todo-app) - Full-featured todo application with validation
198
+ - [Todo App with Vue](examples/vue-todo-app) - Same todo app built with Vue 3
199
+ - [Todo App with React](examples/react-todo-app) - Same todo app built with React
200
+ - More examples coming soon for other frameworks
201
+
202
+ ## 🔍 How It Works
203
+
204
+ When you import a `.server.js` file in your client code, Vite Server Actions:
205
+
206
+ 1. **Intercepts the import** - Replaces server module imports with client proxies
207
+ 2. **Creates proxy functions** - Each exported function becomes a client-side proxy
208
+ 3. **Generates API endpoints** - Maps each function to an HTTP endpoint
209
+ 4. **Handles the transport** - Serializes arguments and return values automatically
210
+
211
+ ```javascript
212
+ // What you write:
213
+ import { getUser } from "./user.server.js";
214
+ const user = await getUser(123);
215
+
216
+ // What runs in the browser:
217
+ const user = await fetch("/api/user/getUser", {
218
+ method: "POST",
219
+ body: JSON.stringify([123]),
220
+ }).then((r) => r.json());
114
221
  ```
115
222
 
116
- ## 🤯 How it works
223
+ ### Development vs Production
117
224
 
118
- Vite Server Actions works by creating an API endpoint for each server function you define.
225
+ - **Development**: Server functions run as Express middleware in Vite's dev server
226
+ - **Production**: Builds to a standalone Express server with all your functions
119
227
 
120
- When you import a server action in your client-side code, Vite Server Actions will intercept the import and return a
121
- proxy function that sends a request to the server endpoint instead of executing the function locally.
228
+ ## ⚙️ Configuration
122
229
 
123
- In development mode, the server is run as a middleware in the Vite dev server, while in production mode, the server is
124
- bundled into a single file that can be run with Node.js.
230
+ ### Common Use Cases
125
231
 
126
- ### Sequence Diagram
232
+ #### Enable Validation & API Documentation
127
233
 
128
- ```mermaid
129
- sequenceDiagram
130
- participant Client
131
- participant Vite Dev Server
132
- participant Plugin Middleware
133
- participant Server Function
134
- participant File System
135
- Client ->> Vite Dev Server: import { addTodo } from './server/todos.server.js'
136
- Vite Dev Server ->> Client: Returns proxied function
137
- Client ->> Client: Call addTodo({ text: 'New todo' })
138
- Client ->> Vite Dev Server: POST /api/todos/addTodo
139
- Vite Dev Server ->> Plugin Middleware: Handle POST request
140
- Plugin Middleware ->> Server Function: Call addTodo function
141
- Server Function ->> File System: Read todos.json
142
- File System ->> Server Function: Return current todos
143
- Server Function ->> Server Function: Add new todo
144
- Server Function ->> File System: Write updated todos.json
145
- File System ->> Server Function: Write confirmation
146
- Server Function ->> Plugin Middleware: Return new todo
147
- Plugin Middleware ->> Vite Dev Server: Send JSON response
148
- Vite Dev Server ->> Client: Return new todo data
234
+ ```javascript
235
+ serverActions({
236
+ validation: {
237
+ enabled: true,
238
+ },
239
+ openAPI: {
240
+ enabled: true,
241
+ swaggerUI: true,
242
+ },
243
+ });
149
244
  ```
150
245
 
151
- ## 🔧 Configuration
246
+ This gives you:
247
+
248
+ - Automatic request validation with Zod schemas
249
+ - OpenAPI spec at `/api/openapi.json`
250
+ - Interactive docs at `/api/docs`
251
+
252
+ #### Add Authentication Middleware
253
+
254
+ ```javascript
255
+ serverActions({
256
+ middleware: [
257
+ // Add auth check to all server actions
258
+ (req, res, next) => {
259
+ if (!req.headers.authorization) {
260
+ return res.status(401).json({ error: "Unauthorized" });
261
+ }
262
+ next();
263
+ },
264
+ ],
265
+ });
266
+ ```
152
267
 
153
- Vite Server Actions works out of the box, but you can customize it:
268
+ #### Custom API Routes
154
269
 
155
270
  ```javascript
156
271
  serverActions({
157
- // Options (coming soon)
272
+ apiPrefix: "/rpc", // Change from /api to /rpc
273
+ routeTransform: (filePath, functionName) => {
274
+ // users.server.js -> /rpc/users.list
275
+ const module = filePath.replace(".server.js", "");
276
+ return `${module}.${functionName}`;
277
+ },
158
278
  });
159
279
  ```
160
280
 
161
- ## 🛠️ Configuration Options
281
+ ### All Configuration Options
282
+
283
+ | Option | Type | Default | Description |
284
+ | ---------------- | ------------ | -------------------- | ------------------------------ |
285
+ | `apiPrefix` | `string` | `"/api"` | URL prefix for all endpoints |
286
+ | `include` | `string[]` | `["**/*.server.js"]` | Files to process |
287
+ | `exclude` | `string[]` | `[]` | Files to ignore |
288
+ | `middleware` | `Function[]` | `[]` | Express middleware stack |
289
+ | `routeTransform` | `Function` | See below | Customize URL generation |
290
+ | `validation` | `Object` | `{ enabled: false }` | Validation settings |
291
+ | `openAPI` | `Object` | `{ enabled: false }` | OpenAPI documentation settings |
162
292
 
163
- TODO: Add configuration options and descriptions
293
+ #### Route Transform Options
164
294
 
165
- | Option | Type | Default | Description |
166
- |------------------|----------------------------------------|-------------|----------------------------------|
167
- | logLevel | 'error' \| 'warn' \| 'info' \| 'debug' | 'info' | Server log level |
168
- | serverPath | string | '/api' | Base path for server endpoints |
169
- | serverPort | number | 3000 | Port for the server |
170
- | serverHost | string | 'localhost' | Host for the server |
171
- | serverMiddleware | (app: Express) => void | - | Custom middleware for the server |
295
+ ```javascript
296
+ import { pathUtils } from "vite-plugin-server-actions";
297
+
298
+ // Available presets:
299
+ pathUtils.createCleanRoute; // (default) auth.server.js /api/auth/login
300
+ pathUtils.createLegacyRoute; // auth.server.js /api/auth_server/login
301
+ pathUtils.createMinimalRoute; // auth.server.js /api/auth.server/login
302
+ ```
172
303
 
173
- ## TODO
304
+ #### Validation Options
174
305
 
175
- This is a proof of concept, and things are still missing, such as:
306
+ | Option | Type | Default | Description |
307
+ | --------- | --------- | ------- | ------------------------------------- |
308
+ | `enabled` | `boolean` | `false` | Enable request validation |
309
+ | `adapter` | `string` | `"zod"` | Validation library adapter (only zod) |
176
310
 
177
- - [ ] Add configuration options
178
- - [ ] Add tests
179
- - [ ] Allow customizing the HTTP method for each action (e.g. `GET`, `POST`, `PUT`, `DELETE`)
180
- - [ ] Make sure name collisions are handled correctly
181
- - [ ] Make sure the actions are only available on the server when running in production mode.
182
- - [ ] Add more examples (Vue, React, etc.)
183
- - [ ] Publish to npm
311
+ #### OpenAPI Options
312
+
313
+ | Option | Type | Default | Description |
314
+ | ----------- | --------- | --------------------- | ----------------------------------------- |
315
+ | `enabled` | `boolean` | `false` | Enable OpenAPI generation |
316
+ | `swaggerUI` | `boolean` | `true` | Enable Swagger UI when OpenAPI is enabled |
317
+ | `info` | `Object` | See below | OpenAPI specification info |
318
+ | `docsPath` | `string` | `"/api/docs"` | Path for Swagger UI |
319
+ | `specPath` | `string` | `"/api/openapi.json"` | Path for OpenAPI JSON spec |
320
+
321
+ Default `info` object:
322
+
323
+ ```javascript
324
+ {
325
+ title: "Server Actions API",
326
+ version: "1.0.0",
327
+ description: "Auto-generated API documentation for Vite Server Actions"
328
+ }
329
+ ```
330
+
331
+ ## 🔍 Built-in Middleware
332
+
333
+ ### Logging Middleware
334
+
335
+ Vite Server Actions includes a built-in logging middleware that provides detailed console output for debugging:
336
+
337
+ ```javascript
338
+ import serverActions, { middleware } from "vite-plugin-server-actions";
339
+
340
+ export default defineConfig({
341
+ plugins: [
342
+ serverActions({
343
+ middleware: middleware.logging,
344
+ }),
345
+ ],
346
+ });
347
+ ```
348
+
349
+ The logging middleware displays:
350
+
351
+ - 🚀 Action trigger details (module, function, endpoint)
352
+ - 📦 Formatted request body with syntax highlighting
353
+ - ✅ Response time and data
354
+ - ❌ Error responses with status codes
355
+
356
+ Example output:
357
+
358
+ ```
359
+ [2024-01-21T10:30:45.123Z] 🚀 Server Action Triggered
360
+ ├─ Module: src_actions_todo
361
+ ├─ Function: addTodo
362
+ ├─ Method: POST
363
+ └─ Endpoint: /api/src_actions_todo/addTodo
364
+
365
+ 📦 Request Body:
366
+ {
367
+ text: 'Buy groceries',
368
+ priority: 'high'
369
+ }
370
+
371
+ ✅ Response sent in 25ms
372
+ 📤 Response data:
373
+ {
374
+ id: 1,
375
+ text: 'Buy groceries',
376
+ priority: 'high',
377
+ completed: false
378
+ }
379
+ ──────────────────────────────────────────────────
380
+ ```
381
+
382
+ ### Custom Middleware
383
+
384
+ You can add your own Express middleware for authentication, validation, etc:
385
+
386
+ ```javascript
387
+ import serverActions from "vite-plugin-server-actions";
388
+
389
+ // Authentication middleware
390
+ const authMiddleware = (req, res, next) => {
391
+ const token = req.headers.authorization;
392
+ if (!token) {
393
+ return res.status(401).json({ error: "Unauthorized" });
394
+ }
395
+ // Verify token...
396
+ next();
397
+ };
398
+
399
+ // CORS middleware
400
+ const corsMiddleware = (req, res, next) => {
401
+ res.header("Access-Control-Allow-Origin", "*");
402
+ res.header("Access-Control-Allow-Methods", "POST");
403
+ res.header("Access-Control-Allow-Headers", "Content-Type");
404
+ next();
405
+ };
406
+
407
+ export default defineConfig({
408
+ plugins: [
409
+ serverActions({
410
+ middleware: [corsMiddleware, authMiddleware],
411
+ }),
412
+ ],
413
+ });
414
+ ```
415
+
416
+ ## ✅ Automatic Validation & Documentation
417
+
418
+ Add validation to any server function by attaching a Zod schema. The plugin automatically validates requests and generates OpenAPI documentation.
419
+
420
+ ### Quick Setup
421
+
422
+ ```javascript
423
+ // vite.config.js
424
+ serverActions({
425
+ validation: {
426
+ enabled: true,
427
+ },
428
+ openAPI: {
429
+ enabled: true,
430
+ swaggerUI: true,
431
+ },
432
+ });
433
+ ```
434
+
435
+ ### Add Validation to Any Function
436
+
437
+ ```javascript
438
+ // server/users.server.js
439
+ import { z } from "zod";
440
+
441
+ const CreateUserSchema = z.object({
442
+ name: z.string().min(2),
443
+ email: z.string().email(),
444
+ role: z.enum(["admin", "user"]).default("user"),
445
+ });
446
+
447
+ export async function createUser(data) {
448
+ // Input is pre-validated - this will never run with invalid data
449
+ const user = await db.users.create({ data });
450
+
451
+ // Send welcome email, etc...
452
+ return user;
453
+ }
454
+
455
+ // Just attach the schema!
456
+ createUser.schema = CreateUserSchema;
457
+ ```
458
+
459
+ ### What You Get
460
+
461
+ 1. **Automatic Validation** - Invalid requests return 400 with detailed errors
462
+ 2. **Type Safety** - Full TypeScript inference from your Zod schemas
463
+ 3. **API Documentation** - Browse and test your API at `/api/docs`
464
+ 4. **OpenAPI Spec** - Machine-readable spec at `/api/openapi.json`
465
+
466
+ ### Advanced Validation
467
+
468
+ ```javascript
469
+ // Handle arrays and complex inputs
470
+ const BulkUpdateSchema = z.array(
471
+ z.object({
472
+ id: z.number(),
473
+ status: z.enum(["active", "inactive"]),
474
+ }),
475
+ );
476
+
477
+ export async function bulkUpdateUsers(updates) {
478
+ // Type: { id: number, status: 'active' | 'inactive' }[]
479
+ return await db.users.updateMany(updates);
480
+ }
481
+ bulkUpdateUsers.schema = BulkUpdateSchema;
482
+
483
+ // Validate multiple parameters
484
+ export async function getDateRange(startDate, endDate) {
485
+ // Validate both parameters
486
+ return await db.analytics.query({ startDate, endDate });
487
+ }
488
+ getDateRange.schema = z.tuple([
489
+ z.string().datetime(), // startDate
490
+ z.string().datetime(), // endDate
491
+ ]);
492
+ ```
493
+
494
+ ## 🚀 Production Deployment
495
+
496
+ ### Building for Production
497
+
498
+ ```bash
499
+ npm run build
500
+ ```
501
+
502
+ This generates:
503
+
504
+ - `dist/server.js` - Your Express server with all endpoints
505
+ - `dist/actions.js` - Bundled server functions
506
+ - `dist/openapi.json` - API specification (if enabled)
507
+ - Client assets with proxy functions
508
+
509
+ ### Running in Production
510
+
511
+ ```bash
512
+ node dist/server.js
513
+ ```
514
+
515
+ Or with PM2:
516
+
517
+ ```bash
518
+ pm2 start dist/server.js --name my-app
519
+ ```
520
+
521
+ ### Environment Variables
522
+
523
+ ```javascript
524
+ // Access environment variables in server functions
525
+ export async function sendEmail(to, subject, body) {
526
+ const apiKey = process.env.SENDGRID_API_KEY;
527
+ // ...
528
+ }
529
+ ```
530
+
531
+ ## 🛡️ Security Considerations
532
+
533
+ ### Server Code Isolation
534
+
535
+ - Server files (`.server.js`) are never bundled into client code
536
+ - Development builds include safety checks to prevent accidental imports
537
+ - Production builds completely separate server and client code
538
+
539
+ ### Best Practices
540
+
541
+ 1. **Never trust client input** - Always validate with Zod schemas
542
+ 2. **Use middleware for auth** - Add authentication checks globally
543
+ 3. **Sanitize file operations** - Be careful with file paths from clients
544
+ 4. **Limit exposed functions** - Only export what clients need
545
+ 5. **Use environment variables** - Keep secrets out of code
546
+
547
+ ### Example: Secure File Access
548
+
549
+ ```javascript
550
+ // ❌ Dangerous - allows arbitrary file access
551
+ export async function readFile(path) {
552
+ return await fs.readFile(path, "utf-8");
553
+ }
554
+
555
+ // ✅ Safe - validates and restricts access
556
+ import { z } from "zod";
557
+
558
+ const FileSchema = z.enum(["report.pdf", "summary.txt"]);
559
+
560
+ export async function readAllowedFile(filename) {
561
+ const safePath = path.join(SAFE_DIR, filename);
562
+ return await fs.readFile(safePath, "utf-8");
563
+ }
564
+ readAllowedFile.schema = FileSchema;
565
+ ```
566
+
567
+ ## 💻 TypeScript Support
568
+
569
+ Vite Server Actions has first-class TypeScript support with automatic type inference:
570
+
571
+ ```typescript
572
+ // server/users.server.ts
573
+ export async function getUser(id: number) {
574
+ return await db.users.findUnique({ where: { id } });
575
+ }
576
+
577
+ // App.tsx - Full type inference!
578
+ import { getUser } from "./server/users.server";
579
+
580
+ const user = await getUser(123); // Type: User | null
581
+ ```
582
+
583
+ ### With Zod Validation
584
+
585
+ ```typescript
586
+ import { z } from "zod";
587
+
588
+ const schema = z.object({
589
+ name: z.string(),
590
+ age: z.number(),
591
+ });
592
+
593
+ export async function createUser(data: z.infer<typeof schema>) {
594
+ return await db.users.create({ data });
595
+ }
596
+ createUser.schema = schema;
597
+ ```
598
+
599
+ ## 🔧 Error Handling
600
+
601
+ Server errors are automatically caught and returned with proper HTTP status codes:
602
+
603
+ ```javascript
604
+ // server/api.server.js
605
+ export async function riskyOperation() {
606
+ throw new Error("Something went wrong");
607
+ }
608
+
609
+ // Client receives:
610
+ // Status: 500
611
+ // Body: { error: "Internal server error", details: "Something went wrong" }
612
+ ```
613
+
614
+ ### Custom Error Responses
615
+
616
+ ```javascript
617
+ export async function authenticate(token) {
618
+ if (!token) {
619
+ const error = new Error("No token provided");
620
+ error.status = 401;
621
+ throw error;
622
+ }
623
+ // ...
624
+ }
625
+ ```
626
+
627
+ ## 🎯 Common Patterns
628
+
629
+ ### Authenticated Actions
630
+
631
+ ```javascript
632
+ // server/auth.server.js
633
+ export async function withAuth(handler) {
634
+ return async (...args) => {
635
+ const token = args[args.length - 1]; // Pass token as last arg
636
+ const user = await verifyToken(token);
637
+ if (!user) throw new Error("Unauthorized");
638
+
639
+ return handler(...args.slice(0, -1), user);
640
+ };
641
+ }
642
+
643
+ // server/protected.server.js
644
+ import { withAuth } from "./auth.server";
645
+
646
+ export const getSecretData = withAuth(async (user) => {
647
+ return await db.secrets.findMany({ userId: user.id });
648
+ });
649
+ ```
650
+
651
+ ### Caching
652
+
653
+ ```javascript
654
+ const cache = new Map();
655
+
656
+ export async function getExpensiveData(key) {
657
+ if (cache.has(key)) {
658
+ return cache.get(key);
659
+ }
660
+
661
+ const data = await expensiveOperation(key);
662
+ cache.set(key, data);
663
+
664
+ // Clear after 5 minutes
665
+ setTimeout(() => cache.delete(key), 5 * 60 * 1000);
666
+
667
+ return data;
668
+ }
669
+ ```
184
670
 
185
671
  ## 🤝 Contributing
186
672
 
187
- Contributions, issues, and feature requests are welcome! Feel free to
188
- check [issues page](https://github.com/helgesverre/vite-plugin-server-actions/issues).
673
+ Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
674
+
675
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
676
+
677
+ ## 📄 License
678
+
679
+ This project is [MIT](LICENSE) licensed.
189
680
 
190
- ## 📝 License
681
+ ---
191
682
 
192
- This project is [MIT](https://opensource.org/licenses/MIT) licensed.
683
+ <p align="center">
684
+ Made with ❤️ by <a href="https://helgesver.re">Helge Sverre</a>
685
+ </p>