vite-plugin-server-actions 0.1.1 → 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/LICENSE +21 -0
- package/README.md +606 -105
- package/index.d.ts +200 -0
- package/package.json +74 -11
- package/src/build-utils.js +101 -0
- package/src/index.js +531 -58
- package/src/middleware.js +89 -0
- package/src/openapi.js +369 -0
- package/src/types.ts +35 -0
- package/src/validation.js +307 -0
- package/.editorconfig +0 -20
- package/.nvmrc +0 -1
- package/.prettierrc +0 -7
- package/examples/todo-app/.prettierrc +0 -17
- package/examples/todo-app/README.md +0 -58
- package/examples/todo-app/index.html +0 -12
- package/examples/todo-app/jsconfig.json +0 -32
- package/examples/todo-app/package.json +0 -22
- package/examples/todo-app/src/App.svelte +0 -155
- package/examples/todo-app/src/actions/auth.server.js +0 -14
- package/examples/todo-app/src/actions/todo.server.js +0 -57
- package/examples/todo-app/src/app.css +0 -0
- package/examples/todo-app/src/main.js +0 -8
- package/examples/todo-app/svelte.config.js +0 -7
- package/examples/todo-app/todos.json +0 -27
- package/examples/todo-app/vite.config.js +0 -12
- package/examples/todo-app/yarn.lock +0 -658
package/README.md
CHANGED
|
@@ -4,181 +4,682 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/vite-plugin-server-actions)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Write server functions. Call them from the client. That's it.**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
client-side code as if they were local functions.
|
|
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.
|
|
11
10
|
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
32
|
+
## ✨ Core Features
|
|
33
|
+
|
|
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
|
|
18
41
|
|
|
19
42
|
## 🚀 Quick Start
|
|
20
43
|
|
|
21
|
-
1. Install
|
|
44
|
+
### 1. Install
|
|
22
45
|
|
|
23
46
|
```bash
|
|
24
|
-
# Install using npm
|
|
25
47
|
npm install vite-plugin-server-actions
|
|
26
|
-
|
|
27
|
-
# Or using yarn
|
|
28
|
-
yarn add vite-plugin-server-actions
|
|
29
48
|
```
|
|
30
49
|
|
|
31
|
-
2.
|
|
50
|
+
### 2. Configure Vite
|
|
32
51
|
|
|
33
52
|
```javascript
|
|
34
53
|
// vite.config.js
|
|
35
54
|
import { defineConfig } from "vite";
|
|
36
|
-
|
|
37
|
-
// Import the plugin
|
|
38
55
|
import serverActions from "vite-plugin-server-actions";
|
|
39
56
|
|
|
40
57
|
export default defineConfig({
|
|
41
58
|
plugins: [
|
|
42
|
-
//
|
|
43
|
-
serverActions(),
|
|
59
|
+
serverActions(), // That's it! Zero config needed
|
|
44
60
|
],
|
|
45
61
|
});
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
### 3. Create a Server Function
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
Any file ending with `.server.js` becomes a server module:
|
|
51
67
|
|
|
52
68
|
```javascript
|
|
53
|
-
//
|
|
54
|
-
import
|
|
55
|
-
import path from "path";
|
|
69
|
+
// actions/todos.server.js
|
|
70
|
+
import { db } from "./database";
|
|
56
71
|
|
|
57
|
-
|
|
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
|
+
}
|
|
58
76
|
|
|
59
|
-
export async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
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
|
+
});
|
|
64
81
|
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 4. Use in Your Client
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
)
|
|
71
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";
|
|
72
123
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
124
|
+
const prisma = new PrismaClient();
|
|
125
|
+
|
|
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
|
+
});
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### File Uploads
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// server/upload.server.js
|
|
145
|
+
import { writeFile } from "fs/promises";
|
|
146
|
+
import path from "path";
|
|
147
|
+
|
|
148
|
+
export async function uploadFile(filename, base64Data) {
|
|
149
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
150
|
+
const filepath = path.join(process.cwd(), "uploads", filename);
|
|
151
|
+
|
|
152
|
+
await writeFile(filepath, buffer);
|
|
153
|
+
return { success: true, path: `/uploads/${filename}` };
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
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();
|
|
76
164
|
}
|
|
77
165
|
```
|
|
78
166
|
|
|
79
|
-
|
|
167
|
+
### With Validation
|
|
80
168
|
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
});
|
|
85
179
|
|
|
86
|
-
|
|
87
|
-
|
|
180
|
+
export async function login(credentials) {
|
|
181
|
+
// Validation happens automatically!
|
|
182
|
+
const user = await db.users.findByEmail(credentials.email);
|
|
88
183
|
|
|
89
|
-
|
|
90
|
-
|
|
184
|
+
if (!user || !(await bcrypt.compare(credentials.password, user.passwordHash))) {
|
|
185
|
+
throw new Error("Invalid credentials");
|
|
91
186
|
}
|
|
92
187
|
|
|
93
|
-
|
|
94
|
-
await saveTodoToJsonFile({ id: Math.random(), text: newTodoText });
|
|
95
|
-
newTodoText = "";
|
|
96
|
-
await fetchTodos();
|
|
188
|
+
return { token: signJWT(user), user };
|
|
97
189
|
}
|
|
98
190
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
191
|
+
// Attach schema for automatic validation
|
|
192
|
+
login.schema = LoginSchema;
|
|
193
|
+
```
|
|
194
|
+
|
|
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());
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Development vs Production
|
|
224
|
+
|
|
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
|
|
227
|
+
|
|
228
|
+
## ⚙️ Configuration
|
|
229
|
+
|
|
230
|
+
### Common Use Cases
|
|
231
|
+
|
|
232
|
+
#### Enable Validation & API Documentation
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
serverActions({
|
|
236
|
+
validation: {
|
|
237
|
+
enabled: true,
|
|
238
|
+
},
|
|
239
|
+
openAPI: {
|
|
240
|
+
enabled: true,
|
|
241
|
+
swaggerUI: true,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
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
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Custom API Routes
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
serverActions({
|
|
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
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
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 |
|
|
292
|
+
|
|
293
|
+
#### Route Transform Options
|
|
294
|
+
|
|
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
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Validation Options
|
|
305
|
+
|
|
306
|
+
| Option | Type | Default | Description |
|
|
307
|
+
| --------- | --------- | ------- | ------------------------------------- |
|
|
308
|
+
| `enabled` | `boolean` | `false` | Enable request validation |
|
|
309
|
+
| `adapter` | `string` | `"zod"` | Validation library adapter (only zod) |
|
|
310
|
+
|
|
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"
|
|
102
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:
|
|
103
350
|
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
106
357
|
|
|
107
|
-
<div>
|
|
108
|
-
<h1>Todos</h1>
|
|
109
|
-
<ul>
|
|
110
|
-
{#each todos as todo}
|
|
111
|
-
<li>
|
|
112
|
-
{todo.text}
|
|
113
|
-
<button on:click="{() => removeTodo(todo.id)}">Remove</button>
|
|
114
|
-
</li>
|
|
115
|
-
{/each}
|
|
116
|
-
</ul>
|
|
117
|
-
<input type="text" bind:value="{newTodoText}" />
|
|
118
|
-
<button on:click="{addTodo}">Add Todo</button>
|
|
119
|
-
</div>
|
|
120
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
|
+
}
|
|
121
370
|
|
|
122
|
-
|
|
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
|
+
```
|
|
123
381
|
|
|
124
|
-
|
|
382
|
+
### Custom Middleware
|
|
125
383
|
|
|
126
|
-
|
|
384
|
+
You can add your own Express middleware for authentication, validation, etc:
|
|
127
385
|
|
|
128
|
-
|
|
386
|
+
```javascript
|
|
387
|
+
import serverActions from "vite-plugin-server-actions";
|
|
129
388
|
|
|
130
|
-
|
|
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
|
+
};
|
|
131
406
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
407
|
+
export default defineConfig({
|
|
408
|
+
plugins: [
|
|
409
|
+
serverActions({
|
|
410
|
+
middleware: [corsMiddleware, authMiddleware],
|
|
411
|
+
}),
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
```
|
|
135
415
|
|
|
136
|
-
|
|
137
|
-
While in _production_, it's bundled into a single file that can be run with Node.js.
|
|
416
|
+
## ✅ Automatic Validation & Documentation
|
|
138
417
|
|
|
139
|
-
|
|
418
|
+
Add validation to any server function by attaching a Zod schema. The plugin automatically validates requests and generates OpenAPI documentation.
|
|
140
419
|
|
|
141
|
-
|
|
420
|
+
### Quick Setup
|
|
142
421
|
|
|
143
422
|
```javascript
|
|
423
|
+
// vite.config.js
|
|
144
424
|
serverActions({
|
|
145
|
-
|
|
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(),
|
|
146
591
|
});
|
|
592
|
+
|
|
593
|
+
export async function createUser(data: z.infer<typeof schema>) {
|
|
594
|
+
return await db.users.create({ data });
|
|
595
|
+
}
|
|
596
|
+
createUser.schema = schema;
|
|
147
597
|
```
|
|
148
598
|
|
|
149
|
-
##
|
|
599
|
+
## 🔧 Error Handling
|
|
150
600
|
|
|
151
|
-
|
|
601
|
+
Server errors are automatically caught and returned with proper HTTP status codes:
|
|
152
602
|
|
|
153
|
-
|
|
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
|
+
}
|
|
154
642
|
|
|
155
|
-
|
|
643
|
+
// server/protected.server.js
|
|
644
|
+
import { withAuth } from "./auth.server";
|
|
156
645
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
- [ ] Make sure the actions are only available on the server when running in production mode.
|
|
162
|
-
- [ ] Add more examples (Vue, React, etc.)
|
|
163
|
-
- [ ] Publish to npm
|
|
646
|
+
export const getSecretData = withAuth(async (user) => {
|
|
647
|
+
return await db.secrets.findMany({ userId: user.id });
|
|
648
|
+
});
|
|
649
|
+
```
|
|
164
650
|
|
|
165
|
-
|
|
651
|
+
### Caching
|
|
166
652
|
|
|
167
|
-
|
|
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
|
+
}
|
|
168
660
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
git clone git@github.com:HelgeSverre/vite-plugin-server-actions.git
|
|
172
|
-
cd vite-plugin-server-actions
|
|
661
|
+
const data = await expensiveOperation(key);
|
|
662
|
+
cache.set(key, data);
|
|
173
663
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
npm run dev
|
|
664
|
+
// Clear after 5 minutes
|
|
665
|
+
setTimeout(() => cache.delete(key), 5 * 60 * 1000);
|
|
177
666
|
|
|
178
|
-
|
|
179
|
-
|
|
667
|
+
return data;
|
|
668
|
+
}
|
|
180
669
|
```
|
|
181
670
|
|
|
182
|
-
##
|
|
671
|
+
## 🤝 Contributing
|
|
672
|
+
|
|
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.
|
|
680
|
+
|
|
681
|
+
---
|
|
183
682
|
|
|
184
|
-
|
|
683
|
+
<p align="center">
|
|
684
|
+
Made with ❤️ by <a href="https://helgesver.re">Helge Sverre</a>
|
|
685
|
+
</p>
|