Перейти к содержимому

Удалённые функции

Доступно с версии 2.27

Удалённые функции — это инструмент для типобезопасного взаимодействия между клиентом и сервером. Их можно вызывать в любой части вашего приложения, но они всегда выполняются на сервере, что позволяет безопасно использовать модули, доступные только на сервере, такие как переменные окружения и клиенты баз данных.

В сочетании с экспериментальной поддержкой Svelte для await это позволяет загружать и обрабатывать данные непосредственно внутри ваших компонентов.

Эта функция на данный момент является экспериментальной, то есть она может содержать ошибки и может быть изменена без предварительного уведомления. Для использования необходимо включить опцию kit.experimental.remoteFunctions в вашем файле svelte.config.js:

svelte.config.js
export default {
kit: {
experimental: {
remoteFunctions: true
}
}
};

Удалённые функции экспортируются из файлов .remote.js или .remote.ts и бывают четырёх типов: query, form, command и prerender. На клиенте экспортированные функции преобразуются в обёртки fetch, которые вызывают свои серверные аналоги через сгенерированную HTTP-точку доступа. Удалённые файлы должны располагаться в директориях lib или routes.

Функция query позволяет получать динамические данные с сервера (для статических данных рекомендуется использовать prerender):

src/routes/blog/data.remote.js
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getPosts = query(async () => {
const posts = await db.sql`
SELECT title, slug
FROM post
ORDER BY published_at
DESC
`;
return posts;
});

Запрос, возвращаемый из getPosts, работает как Promise, который разрешается в posts:

src/routes/blog/+page.svelte
<script>
import { getPosts } from './data.remote';
</script>
<h1>Недавние сообщения</h1>
<ul>
{#each await getPosts() as { title, slug }}
<li><a href="/blog/{slug}">{title}</a></li>
{/each}
</ul>

До завершения промиса (или в случае ошибки) будет вызван ближайший <svelte:boundary>.

Хотя рекомендуется использовать await, в качестве альтернативы запрос также имеет свойства loading, error и current:

src/routes/blog/+page.svelte
<script>
import { getPosts } from './data.remote';
const query = getPosts();
</script>
{#if query.error}
<p>упс!</p>
{:else if query.loading}
<p>загрузка...</p>
{:else}
<ul>
{#each query.current as { title, slug }}
<li><a href="/blog/{slug}">{title}</a></li>
{/each}
</ul>
{/if}

Функции query могут принимать аргументы, например slug отдельной публикации:

src/routes/blog/+page.svelte
<script>
import { getPost } from '../data.remote';
let { params } = $props();
const post = $derived(await getPost(params.slug));
</script>
<h1>{post.title}</h1>
<div>{@html post.content}</div>

Поскольку getPost предоставляет HTTP-эндпойнт, важно проверять этот аргумент, чтобы убедиться в его корректном типе. Для этого можно использовать любую библиотеку валидации из Standard Schema, такую как Zod или Valibot:

src/routes/blog/data.remote.js
import * as v from 'valibot';
import { error } from '@sveltejs/kit';
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => {
const [post] = await db.sql`
SELECT * FROM post
WHERE slug = ${slug}
`;
if (!post) error(404, 'Не найдено');
return post;
});

Как аргументы, так и возвращаемые значения сериализуются с помощью devalue, который поддерживает типы вроде Date и Map (а также пользовательские типы, определённые в вашем хуке transport) в дополнение к JSON.

Любой запрос можно обновить с помощью метода refresh:

<button onclick={() => getPosts().refresh()}>
Проверка новых постов
</button>

query.batch работает аналогично query, за исключением того, что он группирует запросы, происходящие в рамках одной макрозадачи. Это решает так называемую проблему n+1: вместо того чтобы каждый запрос приводил к отдельному вызову базы данных (например), одновременные запросы объединяются.

На сервере callback получает массив аргументов, с которыми была вызвана функция. Он должен возвращать функцию вида (input: Input, index: number) => Output. Затем SvelteKit вызывает эту функцию с каждым из входных аргументов, чтобы разрешить отдельные вызовы с их результатами.

weather.remote.js
import * as v from 'valibot';
import { query } from '$app/server';
import * as db from '$lib/server/database';
export const getWeather = query.batch(v.string(), async (cityIds) => {
const weather = await db.sql`
SELECT * FROM weather
WHERE city_id = ANY(${cityIds})
`;
const lookup = new Map(weather.map(w => [w.city_id, w]));
return (cityId) => lookup.get(cityId);
});
Weather.svelte
<script>
import CityWeather from './CityWeather.svelte';
import { getWeather } from './weather.remote.js';
let { cities } = $props();
let limit = $state(5);
</script>
<h2>Погода</h2>
{#each cities.slice(0, limit) as city}
<h3>{city.name}</h3>
<CityWeather weather={await getWeather(city.id)} />
{/each}
{#if cities.length > limit}
<button onclick={() => limit += 5}>
Загрузить ещё
</button>
{/if}

Функция form упрощает запись данных на сервер. Она принимает колбэк, который получает текущий FormData

src/routes/blog/data.remote.js
import * as v from 'valibot';
import { error, redirect } from '@sveltejs/kit';
import { query, form } from '$app/server';
import * as db from '$lib/server/database';
import * as auth from '$lib/server/auth';
export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { /* ... */ });
export const createPost = form(async (data) => {
// Проверяем, что пользователь авторизован
const user = await auth.getUser();
if (!user) error(401, 'Не авторизован');
const title = data.get('title');
const content = data.get('content');
// Проверяем валидность данных
if (typeof title !== 'string' || typeof content !== 'string') {
error(400, 'Название и содержимое обязательны');
}
const slug = title.toLowerCase().replace(/ /g, '-');
// Добавляем запись в базу данных
await db.sql`
INSERT INTO post (slug, title, content)
VALUES (${slug}, ${title}, ${content})
`;
// Перенаправляем на новую страницу
redirect(303, `/blog/${slug}`);
});

…и возвращает объект, который можно использовать через spread-оператор (...) в элементе <form>. Колбэк вызывается при каждой отправке формы.

src/routes/blog/new/+page.svelte
<script>
import { createPost } from '../data.remote';
</script>
<h1>Создание новой записи</h1>
<form {...createPost}>
<label>
<h2>Заголовок</h2>
<input name="title" />
</label>
<label>
<h2>Текст записи</h2>
<textarea name="content"></textarea>
</label>
<button>Опубликовать!</button>
</form>

Объект формы содержит свойства method и action, позволяющие ей работать без JavaScript (то есть отправлять данные с перезагрузкой страницы). Также у неё есть обработчик onsubmit, который прогрессивно улучшает форму при наличии JavaScript, отправляя данные без перезагрузки всей страницы.

Как и в случае с query, если колбэк использует отправленные данные (data), они должны быть валидированы путём передачи Standard Schema в качестве первого аргумента в form.

Форма состоит из набора полей, которые определяются схемой. В случае с createPost у нас есть два поля, title и content, оба из которых являются строками. Чтобы получить атрибуты поля, вызовите его метод .as(...), указав, какой тип ввода использовать. Для большинства типов ввода вы также можете передать второй аргумент — .as(type, value) — чтобы управлять отображаемым значением:

<form {...createPost}>
<label>
<h2>Заголовок</h2>
<input {...createPost.fields.title.as('text')} />
</label>
<label>
<h2>Текст записи</h2>
<textarea {...createPost.fields.content.as('text')}></textarea>
</label>
<button>Опубликовать!</button>
</form>

Эти атрибуты позволяют SvelteKit устанавливать правильный тип ввода, задавать name, который используется для формирования данных (data), передаваемых в обработчик, заполнять значение (value) формы (например, после неудачной отправки, чтобы избавить пользователя от необходимости вводить всё заново) и устанавливать состояние aria-invalid.

Передача второго аргумента в .as(...) полезна при рендеринге формы на основе существующих данных, таких как форма редактирования или несколько экземпляров, созданных с помощью for(...). Полям ввода radio, submit и hidden это значение требуется всегда, а полям checkbox оно необходимо, когда они представляют один из вариантов в поле-массиве. Поля типа file не могут быть заполнены таким способом.

Поля могут быть вложены в объекты и массивы, а их значениями могут быть строки, числа, логические значения или объекты File. Например, если бы ваша схема выглядела так…

data.remote.js
const datingProfile = v.object({
name: v.string(),
photo: v.file(),
info: v.object({
height: v.number(),
likesDogs: v.optional(v.boolean(), false)
}),
attributes: v.array(v.string())
});
export const createProfile = form(datingProfile, (data) => { /* ... */ });

…ваша форма могла бы выглядеть так:

<script>
import { createProfile } from './data.remote';
const { name, photo, info, attributes } = createProfile.fields;
</script>
<form {...createProfile} enctype="multipart/form-data">
<label>
<input {...name.as('text')} /> Имя
</label>
<label>
<input {...photo.as('file')} /> Фото
</label>
<label>
<input {...info.height.as('number')} /> Рост (см)
</label>
<label>
<input {...info.likesDogs.as('checkbox')} /> Я люблю собак
</label>
<h2>Мои лучшие качества</h2>
<input {...attributes[0].as('text')} />
<input {...attributes[1].as('text')} />
<input {...attributes[2].as('text')} />
<button>Отправить</button>
</form>

Поскольку наша форма содержит поле ввода file, мы добавили атрибут enctype="multipart/form-data". Значения для info.height и info.likesDogs автоматически приводятся к числу и логическому значению соответственно.

В случае с полями ввода radio и checkbox, относящимися к одному и тому же полю, значение value должно быть указано в качестве второго аргумента в .as(...):

data.remote.js
export const operatingSystems = /** @type {const} */ (['windows', 'mac', 'linux']);
export const languages = /** @type {const} */ (['html', 'css', 'js']);
export const survey = form(
v.object({
operatingSystem: v.picklist(operatingSystems),
languages: v.optional(v.array(v.picklist(languages)), []),
}),
(data) => { /* ... */ },
);
<form {...survey}>
<h2>Какую ОС вы используете?</h2>
{#each operatingSystems as os}
<label>
<input {...survey.fields.operatingSystem.as('radio', os)}>
{os}
</label>
{/each}
<h2>На каких языках вы программируете?</h2>
{#each languages as language}
<label>
<input {...survey.fields.languages.as('checkbox', language)}>
{language}
</label>
{/each}
<button>Отправить</button>
</form>

В качестве альтернативы вы можете использовать select и select multiple:

<form {...survey}>
<h2>Какую ОС вы используете?</h2>
<select {...survey.fields.operatingSystem.as('select')}>
{#each operatingSystems as os}
<option>{os}</option>
{/each}
</select>
<h2>На каких языках вы программируете?</h2>
<select {...survey.fields.languages.as('select multiple')}>
{#each languages as language}
<option>{language}</option>
{/each}
</select>
<button>Отправить</button>
</form>

В дополнение к декларативной валидации по схеме, вы можете программно помечать поля как невалидные внутри обработчика формы, используя хелпер invalid из @sveltejs/kit. Это полезно в тех случаях, когда вы не можете знать, валидны ли данные, пока не попытаетесь выполнить какое-либо действие.

  • Он выбрасывает исключение (throw) так же, как redirect или error
  • Он принимает несколько аргументов, которые могут быть строками (для проблем, относящихся к форме в целом — они будут отображаться только в fields.allIssues()) или ошибками, соответствующими Standard Schema (для тех, что относятся к конкретному полю). Используйте параметр issue для типобезопасного создания таких ошибок:
src/routes/shop/data.remote.js
import * as v from 'valibot';
import { invalid } from '@sveltejs/kit';
import { form } from '$app/server';
import * as db from '$lib/server/database';
export const buyHotcakes = form(
v.object({
qty: v.pipe(
v.number(),
v.minValue(1, 'вы должны купить хотя бы один пирожок')
)
}),
async (data, issue) => {
try {
await db.buy(data.qty);
} catch (e) {
if (e.code === 'OUT_OF_STOCK') {
invalid(
issue.qty(`у нас не хватает пирожков`)
);
}
}
}
);

Если отправленные данные не проходят проверку по схеме, колбэк не будет выполнен. Вместо этого метод issues() каждого невалидного поля вернет массив объектов { message: string }, а атрибут aria-invalid (возвращаемый из as(...)) будет установлен в true:

<form {...createPost}>
<label>
<h2>Заголовок</h2>
{#each createPost.fields.title.issues() as issue}
<p class="issue">{issue.message}</p>
{/each}
<input {...createPost.fields.title.as('text')} />
</label>
<label>
<h2>Текст записи</h2>
{#each createPost.fields.content.issues() as issue}
<p class="issue">{issue.message}</p>
{/each}
<textarea {...createPost.fields.content.as('text')}></textarea>
</label>
<button>Опубликовать!</button>
</form>

Вам не нужно ждать отправки формы, чтобы провалидировать данные — вы можете вызывать validate() программно, например, в обработчике oninput (который будет проверять данные при каждом нажатии клавиши) или в обработчике onchange:

<form {...createPost} oninput={() => createPost.validate()}>
<!-- -->
</form>

По умолчанию ошибки будут игнорироваться, если они относятся к элементам управления формы, с которыми еще не взаимодействовали. Чтобы провалидировать все поля ввода, вызовите validate({ includeUntouched: true }).

Для валидации на стороне клиента вы можете указать preflight-схему, которая заполнит issues() и предотвратит отправку данных на сервер, если данные не прошли валидацию:

<script>
import * as v from 'valibot';
import { createPost } from '../data.remote';
const schema = v.object({
title: v.pipe(v.string(), v.nonEmpty()),
content: v.pipe(v.string(), v.nonEmpty())
});
</script>
<h1>Создание новой записи</h1>
<form {...+++createPost.preflight(schema)+++}>
<!-- -->
</form>

Чтобы получить список всех ошибок, а не только тех, которые относятся к одному конкретному полю, вы можете использовать метод fields.allIssues():

{#each createPost.fields.allIssues() as issue}
<p>{issue.message}</p>
{/each}

У каждого поля есть метод value(), который отражает его текущее значение. По мере того как пользователь взаимодействует с формой, оно обновляется автоматически:

<form {...createPost}>
<!-- -->
</form>
<div class="preview">
<h2>{createPost.fields.title.value()}</h2>
<div>{@html render(createPost.fields.content.value())}</div>
</div>

В качестве альтернативы, createPost.fields.value() вернет объект { title, content }.

Вы можете обновить поле (или набор полей) с помощью метода set(...):

<script>
import { createPost } from '../data.remote';
// это...
createPost.fields.set({
title: 'Моя новая запись в блоге',
content: 'Lorem ipsum dolor sit amet...'
});
// ...эквивалентно этому:
createPost.fields.title.set('Моя новая запись в блоге');
createPost.fields.content.set('Lorem ipsum dolor sit amet');
</script>

В случае отправки формы без прогрессивного улучшения (то есть когда JavaScript по какой-либо причине недоступен) метод value() также заполняется, если отправленные данные оказались невалидными. Это сделано для того, чтобы пользователю не пришлось заполнять всю форму заново с нуля.

Вы можете предотвратить отправку конфиденциальных данных (таких как пароли и номера кредитных карт) обратно пользователю, используя имя, начинающееся с нижнего подчеркивания:

<form {...register}>
<label>
Имя пользователя
<input {...register.fields.username.as('text')} />
</label>
<label>
Пароль
<input {...register.fields._password.as('password')} />
</label>
<button>Зарегистрироваться!</button>
</form>

В данном примере, если данные не проходят валидацию, при перезагрузке страницы будет заполнено только первое поле <input>.

По умолчанию после успешной отправки формы автоматически обновляются все запросы на странице (вместе с любыми функциями load). Это гарантирует актуальность данных, но неэффективно: многие запросы останутся неизменными, а для получения обновлённых данных потребуется повторный запрос к серверу.

Вместо этого мы можем указать, какие именно запросы нужно обновить после конкретной отправки формы. Это называется однопроходной мутацией, и есть два способа её реализовать. Первый — обновить запрос на сервере, внутри обработчика формы:

export const getPosts = query(async () => { /* ... */ });
export const getPost = query(v.string(), async (slug) => { /* ... */ });
export const createPost = form(async (data) => {
// логика формы...
// Обновляем `getPosts()` на сервере и отправляем
// данные вместе с результатом `createPost`
await getPosts().refresh();
// Перенаправляем на новую страницу
redirect(303, `/blog/${slug}`);
});

Второй способ — управлять однопроходной мутацией с клиента, что мы рассмотрим в разделе про enhance.

В примере выше используется redirect(...), который перенаправляет пользователя на новую страницу. Альтернативно, колбэк может возвращать данные, которые станут доступны как createPost.result:

src/routes/blog/data.remote.js
export const createPost = form(async (data) => {
// ...
return { success: true };
});
src/routes/blog/new/+page.svelte
<script>
import { createPost } from '../data.remote';
</script>
<h1>Создание новой записи</h1>
<form {...createPost}><!-- ... --></form>
{#if createPost.result?.success}
<p>Запись успешно опубликована!</p>
{/if}

Это значение временное — оно исчезнет при повторной отправке формы, переходе на другую страницу или обновлении страницы.

Если во время отправки произойдёт ошибка, будет отображена ближайшая страница +error.svelte.

Мы можем настроить поведение при отправке формы с помощью метода enhance:

src/routes/blog/new/+page.svelte
<script>
import { createPost } from '../data.remote';
import { showToast } from '$lib/toast';
</script>
<h1>Создание новой записи</h1>
<form {...createPost.enhance(async ({ form, data, submit }) => {
try {
await submit();
form.reset();
showToast('Запись успешно опубликована!');
} catch (error) {
showToast('Ошибка! Что-то пошло не так');
}
})}>
<input name="title" />
<textarea name="content"></textarea>
<button>Опубликовать</button>
</form>

Колбэк получает элемент form, содержащиеся в нём data и функцию submit.

Для реализации однопроходных мутаций на клиенте используйте submit().updates(...). Например, если на этой странице используется запрос getPosts(), мы можем обновить его следующим образом:

await submit().updates(getPosts());

Мы также можем переопределить текущие данные во время выполнения отправки формы:

await submit().updates(
getPosts().withOverride((posts) => [newPost, ...posts])
);

Переопределённые данные применяются немедленно и сбрасываются после завершения (или ошибки) отправки формы.

Некоторые формы могут повторяться как часть списка. В этом случае вы можете создавать отдельные экземпляры функции формы с помощью for(id), чтобы обеспечить их изоляцию.

Если каждый экземпляр должен отображать разные значения, передайте их в качестве второго аргумента в .as(...):

src/routes/todos/+page.svelte
<script>
import { getTodos, modifyTodo } from '../data.remote';
</script>
<h1>Список дел</h1>
{#each await getTodos() as todo}
{@const modify = modifyTodo.for(todo.id)}
<form {...modify}>
<input {...modify.fields.description.as('text', todo.description)} />
<button disabled={!!modify.pending}>Сохранить изменения</button>
</form>
{/each}

В форме <form> можно использовать несколько кнопок отправки. Например, вы можете иметь одну форму, которая позволяет либо войти в систему, либо зарегистрироваться — в зависимости от того, какая кнопка была нажата.

Чтобы это реализовать, добавьте в свою схему поле для значения кнопки и используйте as('submit', value) для его привязки:

src/routes/login/+page.svelte
<script>
import { loginOrRegister } from '$lib/auth';
</script>
<form {...loginOrRegister}>
<label>
Ваш логин
<input {...loginOrRegister.fields.username.as('text')} />
</label>
<label>
Ваш пароль
<input {...loginOrRegister.fields._password.as('password')} />
</label>
<button {...loginOrRegister.fields.action.as('submit', 'login')}>Войти</button>
<button {...loginOrRegister.fields.action.as('submit', 'register')}>Зарегистрироваться</button>
</form>

В обработчике формы вы можете проверить, какая именно кнопка была нажата:

$lib/auth.js
import * as v from 'valibot';
import { form } from '$app/server';
export const loginOrRegister = form(
v.object({
username: v.string(),
_password: v.string(),
action: v.picklist(['login', 'register'])
}),
async ({ username, _password, action }) => {
if (action === 'login') {
// обработка входа
} else {
// обработка регистрации
}
}
);

Функция command, аналогично form, позволяет записывать данные на сервер. В отличие от form, она не привязана к элементу и может вызываться из любого места.

Как и в случае с query, если функция принимает аргумент, его следует валидировать, передав Standard Schema в качестве первого аргумента command.

likes.remote.js
import * as v from 'valibot';
import { query, command } from '$app/server';
import * as db from '$lib/server/database';
export const getLikes = query(v.string(), async (id) => {
const [row] = await db.sql`
SELECT likes
FROM item
WHERE id = ${id}
`;
return row.likes;
});
export const addLike = command(v.string(), async (id) => {
await db.sql`
UPDATE item
SET likes = likes + 1
WHERE id = ${id}
`;
});

Теперь можно просто вызвать addLike, например, из обработчика событий:

+page.svelte
<script>
import { getLikes, addLike } from './likes.remote';
import { showToast } from '$lib/toast';
let { item } = $props();
</script>
<button
onclick={async () => {
try {
await addLike(item.id);
} catch (error) {
showToast('Произошла ошибка!');
}
}}
>
Лайкнуть
</button>
<p>Лайков: {await getLikes(item.id)}</p>

Как и с формами, все запросы на странице (такие как getLikes(item.id) в примере выше) автоматически обновляются после успешного выполнения команды. Но мы можем сделать это более эффективным, указав SvelteKit, какие именно запросы будут затронуты командой, либо внутри самой команды…

likes.remote.js
export const getLikes = query(v.string(), async (id) => { /* ... */ });
export const addLike = command(v.string(), async (id) => {
await db.sql`
UPDATE item
SET likes = likes + 1
WHERE id = ${id}
`;
getLikes(id).refresh();
});

…либо при её вызове:

try {
await addLike(item.id).updates(getLikes(item.id));
} catch (error) {
showToast('Что-то пошло не так!');
}

Как и ранее, мы можем использовать withOverride для оптимистичных обновлений:

try {
await addLike(item.id).updates(
getLikes(item.id).withOverride((n) => n + 1)
);
} catch (error) {
showToast('Что-то пошло не так!');
}

Функция prerender аналогична query, но вызывается во время сборки для предварительного рендеринга результата. Используйте её для данных, которые меняются не чаще одного раза при повторном развёртывании.

src/routes/blog/data.remote.js
import { prerender } from '$app/server';
import * as db from '$lib/server/database';
export const getPosts = prerender(async () => {
const posts = await db.sql`
SELECT title, slug
FROM post
ORDER BY published_at
DESC
`;
return posts;
});

Вы можете использовать функции prerender на страницах, которые в остальном динамические, что позволяет частично предварительно рендерить данные. Это обеспечивает очень быструю навигацию, так как предварительно отрендеренные данные могут храниться на CDN вместе с другими статическими ресурсами.

В браузере предварительно отрендеренные данные сохраняются с помощью API Cache. Этот кеш сохраняется при перезагрузке страницы и очищается при первом посещении новой версии вашего приложения.

Как и в запросах, функции prerender могут принимать аргументы, которые следует проверять с помощью Standard Schema:

src/routes/blog/data.remote.js
import * as v from 'valibot';
import { error } from '@sveltejs/kit';
import { prerender } from '$app/server';
import * as db from '$lib/server/database';
export const getPosts = prerender(async () => { /* ... */ });
export const getPost = prerender(v.string(), async (slug) => {
const [post] = await db.sql`
SELECT * FROM post
WHERE slug = ${slug}
`;
if (!post) error(404, 'Не найдено');
return post;
});

Все вызовы getPost(...), обнаруженные краулером SvelteKit при предварительном рендеринге страниц, будут сохранены автоматически. Однако вы также можете явно указать, с какими значениями следует вызывать функцию, используя параметр inputs:

src/routes/blog/data.remote.js
export const getPost = prerender(
v.string(),
async (slug) => { /* ... */ },
{
inputs: () => [
'first-post',
'second-post',
'third-post'
]
}
);

По умолчанию функции prerender исключаются из серверного бандла, что означает невозможность их вызова с аргументами, которые не были предварительно отрендерены. Вы можете изменить это поведение, установив dynamic: true:

src/routes/blog/data.remote.js
export const getPost = prerender(
v.string(),
async (slug) => { /* ... */ },
{
dynamic: true,
inputs: () => [
'first-post',
'second-post',
'third-post'
]
}
);

Если вы не передаёте невалидные данные в удалённые функции, есть только две причины, по которым аргумент command, query или prerender может не пройти валидацию:

  1. Сигнатура функции изменилась между развёртываниями, и некоторые пользователи используют старую версию приложения
  2. Кто-то пытается атаковать ваш сайт, подбирая данные к вашим эндпойнтам

Во втором случае мы не хотим помогать атакующему, поэтому SvelteKit сгенерирует стандартный ответ 400 Bad Request. Вы можете кастомизировать сообщение, реализовав серверный хук handleValidationError, который, как и handleError, должен возвращать тип App.Error (по умолчанию это { message: string }):

src/hooks.server.js
/** @type {import('@sveltejs/kit').HandleValidationError} */
export function handleValidationError({ event, issues }) {
return {
message: 'Хорошая попытка, хакер!'
};
}

Если вы понимаете, что делаете, и хотите отключить валидацию, вы можете передать строку 'unchecked' вместо схемы:

data.remote.ts
import { query } from '$app/server';
export const getStuff = query('unchecked', async ({ id }: { id: string }) => {
// фактическая структура данных может отличаться от ожидаемой TypeScript,
// так как злоумышленники могут вызывать эту функцию с другими аргументами
});

Внутри query, form и command вы можете использовать getRequestEvent для получения текущего объекта RequestEvent. Это упрощает создание абстракций для работы с файлами куки, например:

user.remote.ts
import { getRequestEvent, query } from '$app/server';
import { findUser } from '$lib/server/database';
export const getProfile = query(async () => {
const user = await getUser();
return {
name: user.name,
avatar: user.avatar
};
});
// эта функция может вызываться из разных мест:
function getUser() {
const { cookies, locals } = getRequestEvent();
locals.userPromise ??= findUser(cookies.get('session_id'));
return await locals.userPromise;
}

Обратите внимание, что некоторые свойства RequestEvent отличаются в удалённых функциях. Отсутствуют params и route.id, нельзя устанавливать заголовки (кроме записи куки, и то только внутри функций form и command), а url.pathname всегда равен / (поскольку фактический путь запроса клиента является внутренней деталью реализации).

Внутри функций query, form и prerender можно использовать функцию redirect(...). В функциях command это нельзя делать, так как перенаправления там следует избегать. (Если это абсолютно необходимо, можно вернуть объект { redirect: location } и обработать его на клиенте.)