Skip to content

HelgeSverre/vite-plugin-server-actions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

83 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⚑ Vite Server Actions

npm version Downloads License: MIT

Write server functions. Call them from the client. That's it.

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.

// server/db.server.js
export async function getUsers() {
  return await database.users.findAll();
}

// App.vue
import { getUsers } from "./server/db.server.js";

const users = await getUsers(); // Just call it!

πŸš€ Why Vite Server Actions?

  • Zero API Boilerplate - No need to define routes, handle HTTP methods, or parse request bodies
  • Type Safety - Full TypeScript support with proper type inference across client-server boundary
  • Built-in Validation - Automatic request validation using Zod schemas
  • Auto Documentation - OpenAPI spec and Swagger UI generated from your code
  • Production Ready - Builds to a standard Node.js Express server
  • Developer Experience - Hot reload, middleware support, and helpful error messages

✨ Core Features

  • πŸ”— Seamless Imports - Import server functions like any other module
  • πŸ›‘οΈ Secure by Default - Server code never exposed to client
  • βœ… Request Validation - Attach Zod schemas for automatic validation
  • πŸ“– API Documentation - Auto-generated OpenAPI specs and Swagger UI
  • πŸ”Œ Middleware Support - Add authentication, logging, CORS, etc.
  • 🎯 Flexible Routing - Customize how file paths map to API endpoints
  • πŸ“¦ Production Optimized - Builds to efficient Express server with all features

πŸš€ Quick Start

1. Install

npm install vite-plugin-server-actions

2. Configure Vite

// vite.config.js
import { defineConfig } from "vite";
import serverActions from "vite-plugin-server-actions";

export default defineConfig({
  plugins: [
    serverActions(), // That's it! Zero config needed
  ],
});

3. Create a Server Function

Any file ending with .server.js becomes a server module:

// actions/todos.server.js
import { db } from "./database";

export async function getTodos(userId) {
  // This runs on the server with full Node.js access
  return await db.todos.findMany({ where: { userId } });
}

export async function addTodo(text, userId) {
  return await db.todos.create({
    data: { text, userId, completed: false },
  });
}

4. Use in Your Client

// App.jsx
import { getTodos, addTodo } from './actions/todos.server.js'

function TodoApp({ userId }) {
  const [todos, setTodos] = useState([])

  useEffect(() => {
    // Just call the server function!
    getTodos(userId).then(setTodos)
  }, [userId])

  async function handleAdd(text) {
    const newTodo = await addTodo(text, userId)
    setTodos([...todos, newTodo])
  }

  return (
    // Your UI here...
  )
}

That's it! The plugin automatically:

  • βœ… Creates API endpoints for each function
  • βœ… Handles serialization/deserialization
  • βœ… Provides full TypeScript support
  • βœ… Works in development and production

πŸ“š Real-World Examples

Database Operations

// server/database.server.js
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

export async function getUser(id) {
  return await prisma.user.findUnique({
    where: { id },
    include: { profile: true },
  });
}

export async function updateUser(id, data) {
  return await prisma.user.update({
    where: { id },
    data,
  });
}

File Uploads

// server/upload.server.js
import { writeFile } from "fs/promises";
import path from "path";

export async function uploadFile(filename, base64Data) {
  const buffer = Buffer.from(base64Data, "base64");
  const filepath = path.join(process.cwd(), "uploads", filename);

  await writeFile(filepath, buffer);
  return { success: true, path: `/uploads/${filename}` };
}

External API Integration

// server/weather.server.js
export async function getWeather(city) {
  const response = await fetch(`https://api.weather.com/v1/current?city=${city}&key=${process.env.API_KEY}`);
  return response.json();
}

With Validation

// server/auth.server.js
import { z } from "zod";
import bcrypt from "bcrypt";
import { signJWT } from "./jwt";

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export async function login(credentials) {
  // Validation happens automatically!
  const user = await db.users.findByEmail(credentials.email);

  if (!user || !(await bcrypt.compare(credentials.password, user.passwordHash))) {
    throw new Error("Invalid credentials");
  }

  return { token: signJWT(user), user };
}

// Attach schema for automatic validation
login.schema = LoginSchema;

Complete Examples

πŸ” How It Works

When you import a .server.js file in your client code, Vite Server Actions:

  1. Intercepts the import - Replaces server module imports with client proxies
  2. Creates proxy functions - Each exported function becomes a client-side proxy
  3. Generates API endpoints - Maps each function to an HTTP endpoint
  4. Handles the transport - Serializes arguments and return values automatically
// What you write:
import { getUser } from "./user.server.js";
const user = await getUser(123);

// What runs in the browser:
const user = await fetch("/api/user/getUser", {
  method: "POST",
  body: JSON.stringify([123]),
}).then((r) => r.json());

Development vs Production

  • Development: Server functions run as Express middleware in Vite's dev server
  • Production: Builds to a standalone Express server with all your functions

βš™οΈ Configuration

Common Use Cases

Enable Validation & API Documentation

serverActions({
  validation: {
    enabled: true,
  },
  openAPI: {
    enabled: true,
    swaggerUI: true,
  },
});

This gives you:

  • Automatic request validation with Zod schemas
  • OpenAPI spec at /api/openapi.json
  • Interactive docs at /api/docs

Add Authentication Middleware

serverActions({
  middleware: [
    // Add auth check to all server actions
    (req, res, next) => {
      if (!req.headers.authorization) {
        return res.status(401).json({ error: "Unauthorized" });
      }
      next();
    },
  ],
});

Custom API Routes

serverActions({
  apiPrefix: "/rpc", // Change from /api to /rpc
  routeTransform: (filePath, functionName) => {
    // users.server.js -> /rpc/users.list
    const module = filePath.replace(".server.js", "");
    return `${module}.${functionName}`;
  },
});

All Configuration Options

Option Type Default Description
apiPrefix string "/api" URL prefix for all endpoints
include string[] ["**/*.server.js"] Files to process
exclude string[] [] Files to ignore
middleware Function[] [] Express middleware stack
routeTransform Function See below Customize URL generation
validation Object { enabled: false } Validation settings
openAPI Object { enabled: false } OpenAPI documentation settings

Route Transform Options

import { pathUtils } from "vite-plugin-server-actions";

// Available presets:
pathUtils.createCleanRoute; // (default) auth.server.js β†’ /api/auth/login
pathUtils.createLegacyRoute; // auth.server.js β†’ /api/auth_server/login
pathUtils.createMinimalRoute; // auth.server.js β†’ /api/auth.server/login

Validation Options

Option Type Default Description
enabled boolean false Enable request validation
adapter string "zod" Validation library adapter (only zod)

OpenAPI Options

Option Type Default Description
enabled boolean false Enable OpenAPI generation
swaggerUI boolean true Enable Swagger UI when OpenAPI is enabled
info Object See below OpenAPI specification info
docsPath string "/api/docs" Path for Swagger UI
specPath string "/api/openapi.json" Path for OpenAPI JSON spec

Default info object:

{
  title: "Server Actions API",
  version: "1.0.0",
  description: "Auto-generated API documentation for Vite Server Actions"
}

πŸ” Built-in Middleware

Logging Middleware

Vite Server Actions includes a built-in logging middleware that provides detailed console output for debugging:

import serverActions, { middleware } from "vite-plugin-server-actions";

export default defineConfig({
  plugins: [
    serverActions({
      middleware: middleware.logging,
    }),
  ],
});

The logging middleware displays:

  • πŸš€ Action trigger details (module, function, endpoint)
  • πŸ“¦ Formatted request body with syntax highlighting
  • βœ… Response time and data
  • ❌ Error responses with status codes

Example output:

[2024-01-21T10:30:45.123Z] πŸš€ Server Action Triggered
β”œβ”€ Module: src_actions_todo
β”œβ”€ Function: addTodo
β”œβ”€ Method: POST
└─ Endpoint: /api/src_actions_todo/addTodo

πŸ“¦ Request Body:
{
  text: 'Buy groceries',
  priority: 'high'
}

βœ… Response sent in 25ms
πŸ“€ Response data:
{
  id: 1,
  text: 'Buy groceries',
  priority: 'high',
  completed: false
}
──────────────────────────────────────────────────

Custom Middleware

You can add your own Express middleware for authentication, validation, etc:

import serverActions from "vite-plugin-server-actions";

// Authentication middleware
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  // Verify token...
  next();
};

// CORS middleware
const corsMiddleware = (req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "POST");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
};

export default defineConfig({
  plugins: [
    serverActions({
      middleware: [corsMiddleware, authMiddleware],
    }),
  ],
});

βœ… Automatic Validation & Documentation

Add validation to any server function by attaching a Zod schema. The plugin automatically validates requests and generates OpenAPI documentation.

Quick Setup

// vite.config.js
serverActions({
  validation: {
    enabled: true,
  },
  openAPI: {
    enabled: true,
    swaggerUI: true,
  },
});

Add Validation to Any Function

// server/users.server.js
import { z } from "zod";

const CreateUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  role: z.enum(["admin", "user"]).default("user"),
});

export async function createUser(data) {
  // Input is pre-validated - this will never run with invalid data
  const user = await db.users.create({ data });

  // Send welcome email, etc...
  return user;
}

// Just attach the schema!
createUser.schema = CreateUserSchema;

What You Get

  1. Automatic Validation - Invalid requests return 400 with detailed errors
  2. Type Safety - Full TypeScript inference from your Zod schemas
  3. API Documentation - Browse and test your API at /api/docs
  4. OpenAPI Spec - Machine-readable spec at /api/openapi.json

Advanced Validation

// Handle arrays and complex inputs
const BulkUpdateSchema = z.array(
  z.object({
    id: z.number(),
    status: z.enum(["active", "inactive"]),
  }),
);

export async function bulkUpdateUsers(updates) {
  // Type: { id: number, status: 'active' | 'inactive' }[]
  return await db.users.updateMany(updates);
}
bulkUpdateUsers.schema = BulkUpdateSchema;

// Validate multiple parameters
export async function getDateRange(startDate, endDate) {
  // Validate both parameters
  return await db.analytics.query({ startDate, endDate });
}
getDateRange.schema = z.tuple([
  z.string().datetime(), // startDate
  z.string().datetime(), // endDate
]);

πŸš€ Production Deployment

Building for Production

npm run build

This generates:

  • dist/server.js - Your Express server with all endpoints
  • dist/actions.js - Bundled server functions
  • dist/openapi.json - API specification (if enabled)
  • Client assets with proxy functions

Running in Production

node dist/server.js

Or with PM2:

pm2 start dist/server.js --name my-app

Environment Variables

// Access environment variables in server functions
export async function sendEmail(to, subject, body) {
  const apiKey = process.env.SENDGRID_API_KEY;
  // ...
}

πŸ›‘οΈ Security Considerations

Server Code Isolation

  • Server files (.server.js) are never bundled into client code
  • Development builds include safety checks to prevent accidental imports
  • Production builds completely separate server and client code

Best Practices

  1. Never trust client input - Always validate with Zod schemas
  2. Use middleware for auth - Add authentication checks globally
  3. Sanitize file operations - Be careful with file paths from clients
  4. Limit exposed functions - Only export what clients need
  5. Use environment variables - Keep secrets out of code

Example: Secure File Access

// ❌ Dangerous - allows arbitrary file access
export async function readFile(path) {
  return await fs.readFile(path, "utf-8");
}

// βœ… Safe - validates and restricts access
import { z } from "zod";

const FileSchema = z.enum(["report.pdf", "summary.txt"]);

export async function readAllowedFile(filename) {
  const safePath = path.join(SAFE_DIR, filename);
  return await fs.readFile(safePath, "utf-8");
}
readAllowedFile.schema = FileSchema;

πŸ’» TypeScript Support

Vite Server Actions has first-class TypeScript support with automatic type inference:

// server/users.server.ts
export async function getUser(id: number) {
  return await db.users.findUnique({ where: { id } });
}

// App.tsx - Full type inference!
import { getUser } from "./server/users.server";

const user = await getUser(123); // Type: User | null

With Zod Validation

import { z } from "zod";

const schema = z.object({
  name: z.string(),
  age: z.number(),
});

export async function createUser(data: z.infer<typeof schema>) {
  return await db.users.create({ data });
}
createUser.schema = schema;

πŸ”§ Error Handling

Server errors are automatically caught and returned with proper HTTP status codes:

// server/api.server.js
export async function riskyOperation() {
  throw new Error("Something went wrong");
}

// Client receives:
// Status: 500
// Body: { error: "Internal server error", details: "Something went wrong" }

Custom Error Responses

export async function authenticate(token) {
  if (!token) {
    const error = new Error("No token provided");
    error.status = 401;
    throw error;
  }
  // ...
}

🎯 Common Patterns

Authenticated Actions

// server/auth.server.js
export async function withAuth(handler) {
  return async (...args) => {
    const token = args[args.length - 1]; // Pass token as last arg
    const user = await verifyToken(token);
    if (!user) throw new Error("Unauthorized");

    return handler(...args.slice(0, -1), user);
  };
}

// server/protected.server.js
import { withAuth } from "./auth.server";

export const getSecretData = withAuth(async (user) => {
  return await db.secrets.findMany({ userId: user.id });
});

Caching

const cache = new Map();

export async function getExpensiveData(key) {
  if (cache.has(key)) {
    return cache.get(key);
  }

  const data = await expensiveOperation(key);
  cache.set(key, data);

  // Clear after 5 minutes
  setTimeout(() => cache.delete(key), 5 * 60 * 1000);

  return data;
}

🀝 Contributing

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.

See CONTRIBUTING.md for development setup and guidelines.

πŸ“„ License

This project is MIT licensed.


Made with ❀️ by Helge Sverre

About

Write server functions. Call them from the client. That's it.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Contributors 2

  •  
  •