Generate ServerRequest types for client-server communication in FOSMVVM. Use when implementing any operation that talks to the server - CRUD operations, data sync, actions, etc. ServerRequest is THE way clients communicate with servers.
/plugin marketplace add foscomputerservices/FOSUtilities/plugin install fosmvvm-generators@fosmvvm-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Generate ServerRequest types for client-server communication.
Architecture context: See FOSMVVMArchitecture.md
ServerRequest is THE way to communicate with an FOSMVVM server. No exceptions.
┌─────────────────────────────────────────────────────────────────────┐
│ ALL CLIENTS USE ServerRequest │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ iOS App: Button tap → request.processRequest(baseURL:) │
│ macOS App: Button tap → request.processRequest(baseURL:) │
│ WebApp: JS → WebApp → request.processRequest(baseURL:) │
│ CLI Tool: main() → request.processRequest(baseURL:) │
│ Data Collector: timer/event → request.processRequest(baseURL:) │
│ Background Job: cron trigger → request.processRequest(baseURL:) │
│ │
└─────────────────────────────────────────────────────────────────────┘
// ❌ WRONG - hardcoded URL
let url = URL(string: "http://server/api/users/123")!
var request = URLRequest(url: url)
// ❌ WRONG - string path
try await client.get("/api/users/\(id)")
// ❌ WRONG - manual JSON encoding
let json = try JSONEncoder().encode(body)
request.httpBody = json
// ❌ WRONG - hardcoded fetch path
fetch('/api/users/123')
// ❌ WRONG - constructing URLs manually
fetch(`/api/ideas/${ideaId}/move`)
// ✅ RIGHT - ServerRequest abstracts everything
let request = UserShowRequest(query: .init(userId: id))
let response = try await request.processRequest(baseURL: serverURL)
// ✅ RIGHT - Create operation
let createRequest = CreateIdeaRequest(requestBody: .init(content: content))
let response = try await createRequest.processRequest(baseURL: serverURL)
// ✅ RIGHT - Update operation
let updateRequest = MoveIdeaRequest(requestBody: .init(ideaId: id, newStatus: status))
let response = try await updateRequest.processRequest(baseURL: serverURL)
The path is derived from the type name. The HTTP method comes from the protocol. You NEVER write URL strings.
If you're about to write URLRequest or a hardcoded path string, STOP and use this skill instead.
| Concern | How ServerRequest Handles It |
|---|---|
| URL Path | Derived from type name via Self.path (e.g., MoveIdeaRequest → /move_idea) |
| HTTP Method | Determined by action.httpMethod (ShowRequest=GET, CreateRequest=POST, etc.) |
| Request Body | RequestBody type, automatically JSON encoded via requestBody?.toJSONData() |
| Response Body | ResponseBody type, automatically JSON decoded into responseBody |
| Validation | RequestBody: ValidatableModel for write operations |
| Type Safety | Compiler enforces correct types throughout |
Choose based on the operation:
| Operation | Protocol | HTTP Method | RequestBody Required? |
|---|---|---|---|
| Read data | ShowRequest | GET | No |
| Read ViewModel | ViewModelRequest | GET | No |
| Create entity | CreateRequest | POST | Yes (ValidatableModel) |
| Update entity | UpdateRequest | PATCH | Yes (ValidatableModel) |
| Replace entity | (use .replace action) | PUT | Yes |
| Soft delete | DeleteRequest | DELETE | No |
| Hard delete | DestroyRequest | DELETE | No |
| File | Location | Purpose |
|---|---|---|
{Action}Request.swift | {ViewModelsTarget}/Requests/ | The ServerRequest type |
{Action}Controller.swift | {WebServerTarget}/Controllers/ | Server-side handler |
| File | Purpose |
|---|---|
| WebApp route | Bridges JS fetch to ServerRequest.fetch() |
| JS handler guidance | How to invoke from browser |
Ask:
Based on operation type:
ShowRequest or ViewModelRequestCreateRequestUpdateRequestDeleteRequest// {Action}Request.swift
import FOSMVVM
public final class {Action}Request: {Protocol}, @unchecked Sendable {
public typealias Query = EmptyQuery // or custom Query type
public typealias Fragment = EmptyFragment
public typealias ResponseError = EmptyError
public let requestBody: RequestBody?
public var responseBody: ResponseBody?
// What the client sends
public struct RequestBody: ServerRequestBody, ValidatableModel {
// Fields...
}
// What the server returns
public struct ResponseBody: {Protocol}ResponseBody {
// Fields (often contains a ViewModel)
}
public init(
query: Query? = nil,
fragment: Fragment? = nil,
requestBody: RequestBody? = nil,
responseBody: ResponseBody? = nil
) {
self.requestBody = requestBody
self.responseBody = responseBody
}
}
// {Action}Controller.swift
import Vapor
import FOSMVVM
import FOSMVVMVapor
final class {Action}Controller: ServerRequestController {
typealias TRequest = {Action}Request
let actions: [ServerRequestAction: ActionProcessor] = [
.{action}: {Action}Request.performAction
]
}
private extension {Action}Request {
static func performAction(
_ request: Vapor.Request,
_ serverRequest: {Action}Request,
_ requestBody: RequestBody
) async throws -> ResponseBody {
let db = request.db
// 1. Fetch/validate
// 2. Perform operation
// 3. Build response (often a ViewModel)
return .init(...)
}
}
// In WebServer routes.swift
try versionedGroup.register(collection: {Action}Controller())
Native apps (iOS, macOS, CLI, etc.):
let request = {Action}Request(requestBody: .init(...))
let response = try await request.processRequest(baseURL: serverURL)
// Use response.responseBody
WebApp (browser clients): See WebApp Bridge Pattern below.
When the client is a web browser, you need a bridge between JavaScript and ServerRequest:
Browser WebApp (Swift) WebServer
│ │ │
│ POST /action-name │ │
│ (JSON body) │ │
│ ─────────────────────────► │ │
│ │ request.processRequest(baseURL:)│
│ │ ────────────────────────────────►│
│ │ ◄────────────────────────────────│
│ ◄──────────────────────── │ (ResponseBody) │
│ (HTML fragment or JSON) │ │
The WebApp route is internal wiring - it's how browsers invoke ServerRequest, just like a button tap invokes it in iOS.
// WebApp routes.swift
app.post("{action-name}") { req async throws -> Response in
// 1. Decode what JS sent
let body = try req.content.decode({Action}Request.RequestBody.self)
// 2. Call server via ServerRequest (NOT hardcoded URL!)
let serverRequest = {Action}Request(requestBody: body)
let response = try await serverRequest.processRequest(baseURL: app.serverBaseURL)
// 3. Return response (HTML fragment or JSON)
// ...
}
async function handle{Action}(data) {
const response = await fetch('/{action-name}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
// Handle response...
}
Note: The JS fetches to the WebApp (same origin), which then uses ServerRequest to talk to the WebServer. The browser NEVER talks directly to the WebServer.
Most operations return a ViewModel for UI update:
public struct ResponseBody: UpdateResponseBody {
public let viewModel: IdeaCardViewModel
}
Some operations just need confirmation:
public struct ResponseBody: CreateResponseBody {
public let id: ModelIdType
}
Delete operations often return nothing:
// Use EmptyBody as ResponseBody
public typealias ResponseBody = EmptyBody
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-12-24 | Initial Kairos-specific skill |
| 2.0 | 2025-12-26 | Complete rewrite: top-down architecture focus, "ServerRequest Is THE Way" principle, generalized from Kairos, WebApp bridge as platform pattern |
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.