Skip to content

Commit 647131e

Browse files
Shadow endpoints (#3679)
* failing test for shadowed pages * find shadowed pages * store endpoint filename * update unit tests * load shadow data * add shadow request handler type * fix types * basic shadow endpoint working with SSR * implement __data.json * serialize shadow props * load shadow props in hydration and client-side navigation * lint * lint * unfocus test * handle redirects * more tests * generate __data.json files when prerendering * prevent prerendering of pages shadowed by non-GET methods * lint * changeset * remove old shadowing test * start simplifying todos page in default template * content negotiation * mark shadow endpoint as dependency of page * lint * revert changes for CI * tweak enhance signature * update docs * more docs * mention accept: application/json * undo template change that was for local testing * Update documentation/docs/03-loading.md * Update packages/create-svelte/templates/default/svelte.config.js Co-authored-by: Conduitry <[email protected]> * safer DATA_SUFFIX * remove comment, we decided on a 405 Co-authored-by: Conduitry <[email protected]>
1 parent f01dd87 commit 647131e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+927
-425
lines changed

.changeset/yellow-coins-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
Implement shadow endpoints

documentation/docs/01-routing.md

Lines changed: 114 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,23 @@ A file called either `src/routes/about.svelte` or `src/routes/about/index.svelte
3939
<p>TODO...</p>
4040
```
4141

42-
Dynamic parameters are encoded using `[brackets]`. For example, a blog post might be defined by `src/routes/blog/[slug].svelte`. Soon, we'll see how to access that parameter in a [load function](#loading) or the [page store](#modules-$app-stores).
42+
Dynamic parameters are encoded using `[brackets]`. For example, a blog post might be defined by `src/routes/blog/[slug].svelte`.
4343

4444
A file or directory can have multiple dynamic parts, like `[id]-[category].svelte`. (Parameters are 'non-greedy'; in an ambiguous case like `x-y-z`, `id` would be `x` and `category` would be `y-z`.)
4545

4646
### Endpoints
4747

48-
Endpoints are modules written in `.js` (or `.ts`) files that export functions corresponding to HTTP methods.
48+
Endpoints are modules written in `.js` (or `.ts`) files that export functions corresponding to HTTP methods. Their job is to allow pages to read and write data that is only available on the server (for example in a database, or on the filesystem).
4949

5050
```ts
51-
// Declaration types for Endpoints
52-
// * declarations that are not exported are for internal use
51+
// Type declarations for endpoints (declarations marked with
52+
// an `export` keyword can be imported from `@sveltejs/kit`)
53+
54+
export interface RequestHandler<Output = Record<string, any>> {
55+
(event: RequestEvent): MaybePromise<
56+
Either<Output extends Response ? Response : EndpointOutput<Output>, Fallthrough>
57+
>;
58+
}
5359

5460
export interface RequestEvent {
5561
request: Request;
@@ -59,43 +65,34 @@ export interface RequestEvent {
5965
platform: App.Platform;
6066
}
6167

62-
type Body = JSONValue | Uint8Array | ReadableStream | stream.Readable;
63-
export interface EndpointOutput<Output extends Body = Body> {
68+
export interface EndpointOutput<Output = Record<string, any>> {
6469
status?: number;
6570
headers?: Headers | Partial<ResponseHeaders>;
66-
body?: Output;
71+
body?: Record<string, any>;
6772
}
6873

6974
type MaybePromise<T> = T | Promise<T>;
75+
7076
interface Fallthrough {
7177
fallthrough: true;
7278
}
73-
74-
export interface RequestHandler<Output extends Body = Body> {
75-
(event: RequestEvent): MaybePromise<
76-
Either<Output extends Response ? Response : EndpointOutput<Output>, Fallthrough>
77-
>;
78-
}
7979
```
8080

8181
> See the [TypeScript](#typescript) section for information on `App.Locals` and `App.Platform`.
8282
83-
For example, our hypothetical blog page, `/blog/cool-article`, might request data from `/blog/cool-article.json`, which could be represented by a `src/routes/blog/[slug].json.js` endpoint:
83+
A page like `src/routes/items/[id].svelte` could get its data from `src/routes/items/[id].js`:
8484

8585
```js
8686
import db from '$lib/database';
8787

8888
/** @type {import('@sveltejs/kit').RequestHandler} */
8989
export async function get({ params }) {
90-
// the `slug` parameter is available because this file
91-
// is called [slug].json.js
92-
const article = await db.get(params.slug);
90+
// `params.id` comes from [id].js
91+
const item = await db.get(params.id);
9392

94-
if (article) {
93+
if (item) {
9594
return {
96-
body: {
97-
article
98-
}
95+
body: { item }
9996
};
10097
}
10198

@@ -105,7 +102,7 @@ export async function get({ params }) {
105102
}
106103
```
107104

108-
> All server-side code, including endpoints, has access to `fetch` in case you need to request data from external APIs.
105+
> All server-side code, including endpoints, has access to `fetch` in case you need to request data from external APIs. Don't worry about the `$lib` import, we'll get to that [later](#modules-$lib).
109106
110107
The job of this function is to return a `{ status, headers, body }` object representing the response, where `status` is an [HTTP status code](https://httpstatusdogs.com):
111108

@@ -114,30 +111,94 @@ The job of this function is to return a `{ status, headers, body }` object repre
114111
- `4xx` — client error
115112
- `5xx` — server error
116113

117-
If the returned `body` is an object, and no `content-type` header is returned, it will automatically be turned into a JSON response. (Don't worry about `$lib`, we'll get to that [later](#modules-$lib).)
118-
119114
> If `{fallthrough: true}` is returned SvelteKit will [fall through](#routing-advanced-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.
120115
121-
For endpoints that handle other HTTP methods, like POST, export the corresponding function:
116+
The returned `body` corresponds to the page's props:
117+
118+
```svelte
119+
<script>
120+
// populated with data from the endpoint
121+
export let item;
122+
</script>
123+
124+
<h1>{item.title}</h1>
125+
```
126+
127+
#### POST, PUT, PATCH, DELETE
128+
129+
Endpoints can handle any HTTP method — not just `GET` — by exporting the corresponding function:
122130

123131
```js
124132
export function post(event) {...}
133+
export function put(event) {...}
134+
export function patch(event) {...}
135+
export function del(event) {...} // `delete` is a reserved word
125136
```
126137

127-
Since `delete` is a reserved word in JavaScript, DELETE requests are handled with a `del` function.
138+
These functions can, like `get`, return a `body` that will be passed to the page as props. Whereas 4xx/5xx responses from `get` will result in an error page rendering, similar responses to non-GET requests do not, allowing you to do things like render form validation errors:
128139

129-
> We don't interact with the `req`/`res` objects you might be familiar with from Node's `http` module or frameworks like Express, because they're only available on certain platforms. Instead, SvelteKit translates the returned object into whatever's required by the platform you're deploying your app to.
140+
```js
141+
// src/routes/items.js
142+
import * as db from '$lib/database';
130143

131-
To set multiple cookies in a single set of response headers, you can return an array:
144+
export async function get() {
145+
const items = await db.list();
132146

133-
```js
134-
return {
135-
headers: {
136-
'set-cookie': [cookie1, cookie2]
147+
return {
148+
body: { items }
149+
};
150+
}
151+
152+
export async function post({ request }) {
153+
const [errors, item] = await db.create(request);
154+
155+
if (errors) {
156+
// return validation errors
157+
return {
158+
status: 400,
159+
body: { errors }
160+
};
137161
}
138-
};
162+
163+
// redirect to the newly created item
164+
return {
165+
status: 303,
166+
headers: {
167+
location: `/items/${item.id}`
168+
}
169+
};
170+
}
171+
```
172+
173+
```svelte
174+
<!-- src/routes/items.svelte -->
175+
<script>
176+
// The page always has access to props from `get`...
177+
export let items;
178+
179+
// ...plus props from `post` when the page is rendered
180+
// in response to a POST request, for example after
181+
// submitting the form below
182+
export let errors;
183+
</script>
184+
185+
{#each items as item}
186+
<Preview item={item}/>
187+
{/each}
188+
189+
<form method="post">
190+
<input name="title">
191+
192+
{#if errors?.title}
193+
<p class="error">{errors.title}</p>
194+
{/if}
195+
196+
<button type="submit">Create item</button>
197+
</form>
139198
```
140199

200+
If you request the route with an `accept: application/json` header, SvelteKit will render the endpoint data as JSON, rather than the page as HTML.
201+
141202
#### Body parsing
142203

143204
The `request` object is an instance of the standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) class. As such, accessing the request body is easy:
@@ -148,6 +209,18 @@ export async function post({ request }) {
148209
}
149210
```
150211

212+
#### Setting cookies
213+
214+
Endpoints can set cookies by returning a `headers` object with `set-cookie`. To set multiple cookies simultaneously, return an array:
215+
216+
```js
217+
return {
218+
headers: {
219+
'set-cookie': [cookie1, cookie2]
220+
}
221+
};
222+
```
223+
151224
#### HTTP method overrides
152225

153226
HTML `<form>` elements only support `GET` and `POST` methods natively. You can allow other methods, like `PUT` and `DELETE`, by specifying them in your [configuration](#configuration-methodoverride) and adding a `_method=VERB` parameter (you can configure the name) to the form's `action`:
@@ -171,15 +244,21 @@ export default {
171244

172245
> Using native `<form>` behaviour ensures your app continues to work when JavaScript fails or is disabled.
173246
247+
### Standalone endpoints
248+
249+
Most commonly, endpoints exist to provide data to the page with which they're paired. They can, however, exist separately from pages. Standalone endpoints have slightly more flexibility over the returned `body` type — in addition to objects, they can return a string or a `Uint8Array`.
250+
251+
> Support for streaming request and response bodies is [coming soon](https://github.com/sveltejs/kit/issues/3419).
252+
174253
### Private modules
175254

176255
Files and directories with a leading `_` or `.` (other than [`.well-known`](https://en.wikipedia.org/wiki/Well-known_URI)) are private by default, meaning that they do not create routes (but can be imported by files that do). You can configure which modules are considered public or private with the [`routes`](#configuration-routes) configuration.
177256

178-
### Advanced
257+
### Advanced routing
179258

180259
#### Rest parameters
181260

182-
A route can have multiple dynamic parameters, for example `src/routes/[category]/[item].svelte` or even `src/routes/[category]-[item].svelte`. If the number of route segments is unknown, you can use rest syntax — for example you might implement GitHub's file viewer like so...
261+
A route can have multiple dynamic parameters, for example `src/routes/[category]/[item].svelte` or even `src/routes/[category]-[item].svelte`. (Parameters are 'non-greedy'; in an ambiguous case like `/x-y-z`, `category` would be `x` and `item` would be `y-z`.) If the number of route segments is unknown, you can use rest syntax — for example you might implement GitHub's file viewer like so...
183262

184263
```bash
185264
/[org]/[repo]/tree/[branch]/[...file]

documentation/docs/03-loading.md

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@
22
title: Loading
33
---
44

5-
A component that defines a page or a layout can export a `load` function that runs before the component is created. This function runs both during server-side rendering and in the client, and allows you to get data for a page without (for example) showing a loading spinner and fetching data in `onMount`.
5+
A component that defines a page or a layout can export a `load` function that runs before the component is created. This function runs both during server-side rendering and in the client, and allows you to fetch and manipulate data before the page is rendered, thus preventing loading spinners.
6+
7+
If the data for a page comes from its endpoint, you may not need a `load` function. It's useful when you need more flexibility, for example loading data from an external API.
68

79
```ts
8-
// Declaration types for Loading
9-
// * declarations that are not exported are for internal use
10+
// Type declarations for `load` (declarations marked with
11+
// an `export` keyword can be imported from `@sveltejs/kit`)
12+
13+
export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
14+
(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
15+
}
1016

1117
export interface LoadInput<Params extends Record<string, string> = Record<string, string>> {
1218
url: URL;
1319
params: Params;
20+
props: Record<string, any>;
1421
fetch(info: RequestInfo, init?: RequestInit): Promise<Response>;
1522
session: App.Session;
1623
stuff: Partial<App.Stuff>;
@@ -26,45 +33,36 @@ export interface LoadOutput<Props extends Record<string, any> = Record<string, a
2633
}
2734

2835
type MaybePromise<T> = T | Promise<T>;
36+
2937
interface Fallthrough {
3038
fallthrough: true;
3139
}
32-
33-
export interface Load<Params = Record<string, string>, Props = Record<string, any>> {
34-
(input: LoadInput<Params>): MaybePromise<Either<Fallthrough, LoadOutput<Props>>>;
35-
}
3640
```
3741

3842
> See the [TypeScript](#typescript) section for information on `App.Session` and `App.Stuff`.
3943
40-
Our example blog page might contain a `load` function like the following:
44+
A page that loads data from an external API might look like this:
4145

4246
```html
47+
<!-- src/routes/blog/[slug].svelte -->
4348
<script context="module">
4449
/** @type {import('@sveltejs/kit').Load} */
4550
export async function load({ params, fetch, session, stuff }) {
46-
const url = `/blog/${params.slug}.json`;
47-
const res = await fetch(url);
48-
49-
if (res.ok) {
50-
return {
51-
props: {
52-
article: await res.json()
53-
}
54-
};
55-
}
51+
const response = await fetch(`https://cms.example.com/article/${params.slug}.json`);
5652
5753
return {
58-
status: res.status,
59-
error: new Error(`Could not load ${url}`)
54+
status: response.status,
55+
props: {
56+
article: response.ok && (await response.json())
57+
}
6058
};
6159
}
6260
</script>
6361
```
6462

6563
> Note the `<script context="module">` — this is necessary because `load` runs before the component is rendered. Code that is per-component instance should go into a second `<script>` tag.
6664
67-
`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that it runs on both the server and the client.
65+
`load` is similar to `getStaticProps` or `getServerSideProps` in Next.js, except that it runs on both the server and the client. In the example above, if a user clicks on a link to this page the data will be fetched from `cms.example.com` without going via our server.
6866

6967
If `load` returns `{fallthrough: true}`, SvelteKit will [fall through](#routing-advanced-fallthrough-routes) to other routes until something responds, or will respond with a generic 404.
7068

@@ -88,7 +86,7 @@ It is recommended that you not store pre-request state in global variables, but
8886
8987
### Input
9088

91-
The `load` function receives an object containing five fields — `url`, `params`, `fetch`, `session` and `stuff`. The `load` function is reactive, and will re-run when its parameters change, but only if they are used in the function. Specifically, if `url`, `session` or `stuff` are used in the function, they will be re-run whenever their value changes, and likewise for the individual properties of `params`.
89+
The `load` function receives an object containing five fields — `url`, `params`, `props`, `fetch`, `session` and `stuff`. The `load` function is reactive, and will re-run when its parameters change, but only if they are used in the function. Specifically, if `url`, `session` or `stuff` are used in the function, they will be re-run whenever their value changes, and likewise for the individual properties of `params`.
9290

9391
> Note that destructuring parameters in the function declaration is enough to count as using them.
9492
@@ -111,6 +109,10 @@ For a route filename example like `src/routes/a/[b]/[...c]` and a `url.pathname`
111109
}
112110
```
113111

112+
#### props
113+
114+
If the page you're loading has an endpoint, the data returned from it is accessible inside the leaf component's `load` function as `props`. For layout components and pages without endpoints, `props` will be an empty object.
115+
114116
#### fetch
115117

116118
`fetch` is equivalent to the native `fetch` web API, and can make credentialed requests. It can be used across both client and server contexts.

documentation/docs/04-hooks.md

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@ This function runs every time SvelteKit receives a request — whether that happ
1515
If unimplemented, defaults to `({ event, resolve }) => resolve(event)`.
1616

1717
```ts
18-
// Declaration types for Hooks
19-
// * declarations that are not exported are for internal use
20-
21-
// type of string[] is only for set-cookie
22-
// everything else must be a type of string
23-
type ResponseHeaders = Record<string, string | string[]>;
18+
// Type declarations for `handle` (declarations marked with
19+
// an `export` keyword can be imported from `@sveltejs/kit`)
2420

2521
export interface RequestEvent {
2622
request: Request;
@@ -86,7 +82,6 @@ During development, if an error occurs because of a syntax error in your Svelte
8682
If unimplemented, SvelteKit will log the error with default formatting.
8783

8884
```ts
89-
// Declaration types for handleError hook
9085
export interface HandleError {
9186
(input: { error: Error & { frame?: string }; event: RequestEvent }): void;
9287
}
@@ -109,7 +104,6 @@ This function takes the `event` object and returns a `session` object that is [a
109104
If unimplemented, session is `{}`.
110105

111106
```ts
112-
// Declaration types for getSession hook
113107
export interface GetSession {
114108
(event: RequestEvent): MaybePromise<App.Session>;
115109
}
@@ -142,8 +136,6 @@ This function allows you to modify (or replace) a `fetch` request for an externa
142136
For example, your `load` function might make a request to a public URL like `https://api.yourapp.com` when the user performs a client-side navigation to the respective page, but during SSR it might make sense to hit the API directly (bypassing whatever proxies and load balancers sit between it and the public internet).
143137

144138
```ts
145-
// Declaration types for externalFetch hook
146-
147139
export interface ExternalFetch {
148140
(req: Request): Promise<Response>;
149141
}

packages/create-svelte/templates/default/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"scripts": {
66
"dev": "svelte-kit dev",
77
"build": "svelte-kit build --verbose",
8-
"preview": "svelte-kit preview",
9-
"start": "svelte-kit start"
8+
"package": "svelte-kit package",
9+
"preview": "svelte-kit preview"
1010
},
1111
"devDependencies": {
1212
"@sveltejs/adapter-auto": "workspace:*",

0 commit comments

Comments
 (0)