Closed Rich-Harris closed 2 years ago
I really like that context="module"
isn't needed in this. :+1:
Some things to discuss for "get-after-post"
What happens if the get or post fail. The more interesting case is post succeeds and get fails (e.g. shaky network)
get only runs after post has returned, adding another roundtrip resulting in a delayed response. Is this still a net-win because the get would have happened next either way and we saved the client->kit-server part for that?
how would developers be able to opt out of this if the additional get request would cause issues for the underlying api services.
Good questions!
The more interesting case is post succeeds and get fails (e.g. shaky network)
To clarify, get-after-post doesn't mean the client would be making any additional requests — everything would be happening on the server, which would coalesce the post
and get
return values and use that to render the page. It's certainly possible that the get
handler could fail (an external API goes down, or the database fails) but we don't have to deal with the scenario where the user goes into the tunnel between post
being called and get
being called — as far as the client <-> SvelteKit app relationship is concerned, everything is happening in a single round trip.
We can't compare the cost of calling post
+get
with the cost of just calling post
because it simply wouldn't be possible to render the shadowed page without the get
— there'd be no data. But we can compare it to the nearest equivalent available today, which is posting to a separate endpoint that redirects back to the current page — e.g. we post from /todos
to /todos.json
, which updates the database then redirects back to /todos
.
In that scenario, we call the /todos.json
post
handler, the browser receives the 303 response, then the browser makes a request for /todos
, which results in Kit calling the /todos.json
get
handler. We've called the same handlers the same number of times, but we've rendered the updated /todos
page in two roundtrips instead of one. So it's absolutely a net win.
(That's for the case where we're relying on the browser's default behaviour. If we're progressively enhancing a la #3533, then the POST
request will only invoke the post
handler, not post
+ get
, because we're not rendering a shadowed page.)
how would developers be able to opt out of this if the additional get request would cause issues for the underlying api services
In the rare case where it really is appropriate for get
to skip doing any work, you could perhaps do it like this:
export async function get({ request }) {
if (request.method !== 'GET') return {};
// ...
}
The post
handler could pass data to the get
handler via event.locals
, if necessary. Would be unusual though I think.
Huge fan of this proposal. I've often thought the load
boilerplate felt a bit hefty..."oh, I have to go do this again?" In fact, I've set up my own load wrappers trying to simplify, but it still requires boilerplate.
Love this.
Wow, this does look super clean. However, I am a bit worried if this behavior might not actually be too clean.
Sure, there is quite a lot of load boilerplate even for the simplest use cases, but this comes with the upside of being very obvious to parse what's going on. With one look you know that the props for this route come from the endpoint that is fetched inside the load function.
A shadow endpoint
makes this more invisible, and I think especially for beginners this could be quite the head scratcher. The svemix approach of mixing front and backend code for small endpoints comes with its own set of downsides, but to me it is more "obvious" in terms of data flow.
The extra learning gap is unfortunate, but I think it shouldn't be too hard to pick up particularly if you see it in a project (or in the demo project). The fact that there are two files with the same name should tip you off that something is going on. A quick trip to the docs should then set you straight.
While there are some hairy details, I think the conceptual "the endpoint gives props to the page" is simple enough, and the details can then be filled in.
If you start with an empty Kit project and somehow don't come over this, the worst case scenario is basically that you revert to the current situation of explicitly loading props.
And besides the learning gap I think this is great. I'd love to cut that boilerplate in all my pages.
I like this a lot and it addresses most of the key concerns of #3483.
I especially like the fact that one can access serverside resources (e.g. a database) almost directly.
The problem with load
is that it is executed on both the server and the client, which means that one has to deal with this limitation. Often the solution is to create actual endpoints, which can be called from both the server and the client. However, this means that one has to explicitly maintain an "untyped" URL as well as the "untyped" response.
Thus, this solution is a significant improvement on load
.
Would there be an equivalent for layouts load functions? Something like a __layout.js
endpoint that populates the layout component with the returned props?
First of all, love that you're thinking about this. This has been one of my key frustrations with SvelteKit. The solution you've come to is basically what I ended up with, too — routes can either return JSON or HTML depending on the accept header.
Second, regarding this comment:
A
shadow endpoint
makes this more invisible, and I think especially for beginners this could be quite the head scratcher.
I think that might be true. There would definitely be a bit more magic to learn, but there's already magic in knowing about [slug]
routes for example, or __layout
. The tradeoff is the confusion about where data is coming from (mostly for beginners) vs. removing 100% of boilerplate for the most common case. I think the latter is a good way to make more developers love SvelteKit — common things are easy, complicated things are possible.
But maybe there's a one-liner that could be added to make it explicit? Not sure what that would look like.
@Glench In the documentation, write in one sentence that shadow endpoint is similar to __layout and by analogy even a novice will know what is going on.
To @f-elix's point:
Would there be an equivalent for layouts load functions? Something like a
__layout.js
endpoint that populates the layout component with the returned props?
That probably makes sense, yeah. But I think it might turn out to be a bit of a design rabbit hole because the word __layout
makes very little sense in that context, since it's really a UI word (it's already a bit weird, since layouts can do non-UI things like redirect and populate stuff
), and we probably need to come up with some better nomenclature and/or generally tidy up the conceptual landscape. Rather than try to solve all that immediately, it's probably better to let stuff percolate for a minute and come up with proposals in a new issue.
@Rich-Harris Thanks so much for implementing this so fast, this is a game changer!
if a route maps to both a page and an endpoint
- this encourages having both files in the same place as opposed to separating into an api
folder. Will this be the way forward ?
This change broke an important use case for us. For certain pages, we use endpoints to proxy HTML content. This allows us to display custom HTML pages for certain users, while having a normal SvelteKit based page for the rest.
If the shadow endpoint's response has a header of Content-Type: text/html
could it bypass this system? Currently it's just throwing a 500 without any logs so I'm unsure what the issue is exactly, but I'm assuming it's trying and failing to parse the body as JSON.
I made a quick PR to show how this could potentially be done, if there's interest in the feature I can finish it up and add tests/fix types/etc.
Amazed this was shipped so fast, I came across the issue a week ago then checked the docs today for an unrelated thing and saw it documented as the default!
I ran into a few rough edges:
__layout
and __error
, both of which are very common places to fetch data for a headless static site. This limitation isn't documented so it took me a minute to figure out why things had blown upindex.ts
or index.json.ts
to behave as a shadow endpoint for the root index.svelte
results in 500 errors whenever returning to the /
after initial load, error in console is failing to fetch http://__data.json
. I think if this is a compile error that's a huge gotcha for shadow endpoints, everybody needs a home pagegetStaticProps
and coI also really like this new feature. What we also do often is to load some data in __layout
and pass it to all other routes via stuff
.
Sadly stuff
isn't available in the shadow endpoints, so we still have to resort to using the load
approach… I didn't check the implementation details to see if that's just an oversight, or impossible due to the architecture of things?
Unfortunately, this has broken the static adapter. Please see #3833
Sorry for posting in a closed discussion, arrived here after getting the link from the Discord server while asking for some hints on when to use load()
and when to use get()
(in a page endpoint).
After reading the initial description, it seems to me page endpoints are a simplified version of load()
- not syntactic sugar per-se, as they compile to something different, but a way to do the same thing with less code.
I think this is worth clarifying in the documentation: Some clear-cut pointers on what to use and when.
The question I'm trying to understand specifically is, What does
load()give you access to that page endpoints don't in terms of context?
(e.g. one is Server-side call, the other can be both server and client side, or access to the url params maybe or other stuff)
I'd even go as far as having a table with use-cases as I predict some folks will have similar questions..
thanks!
Yes, the documentation should be more precise and detailed. I'm confused, I currently don't know where to locate the data retrieval from the external server. I'd like to have a centralized place where I handle external APIs and potential errors, but I guess I have to spread everything across multiple endpoints? I don't know. I feel chaos.
@gkatsanos the page endpoint code always runs on the server. So if you access a protected API (eg. a database or similar which involves credentials that should not leak to the frontend), you should always use page-endpoints.
In the page-endpoints, you do not have access to the stuff
data from your __layout.svelte
routes #3860 and you can only send serializable data.
On the other hand it's basically impossible to build proper form-handling without using page-endpoints.
Personally, I think page-endpoints should be preferred and only use load
when you need to pass data from __layout.svelte
to your routes (using stuff
) or when you need to load data that is not serializable in your load
function (eg. dynamically importing modules).
Of course you can also combine the page-endpoint
and load
, as demonstrated here: https://github.com/sveltejs/kit/issues/3860#issuecomment-1057543382
Describe the problem
A significant number of pages have
load
functions that are essentially just boilerplate:The
load
function even contains a bug — it doesn't handle the 404 case.While we definitely do need
load
for more complex use cases, it feels like we could drastically simplify this. This topic has come up before (e.g. #758), but we got hung up on the problems created by putting endpoint code inside a Svelte file (you need magic opinionated treeshaking, and scoping becomes nonsensical).Describe the proposed solution
I think we can solve the problem very straightforwardly: if a route maps to both a page and an endpoint, we regard the endpoint (hereafter 'shadow endpoint') as providing the data to the page. In other words,
/blog/[slug]
contains both the data (JSON) and the view of the data (HTML), depending on the request'sAccept
header. (This isn't a novel idea; this is how HTTP has always worked.)In the example above, no changes to the endpoint would be necessary other than renaming
[slug].json.js
to[slug].js
. The page, meanwhile, could get rid of the entirecontext="module"
script.One obvious constraint: handlers in shadow endpoints need to return an object of props. Since we already handle serialization to JSON, this is already idiomatic usage, and can easily be enforced.
POST requests
This becomes even more useful with
POST
. Currently, SvelteKit can't render a page as a result of aPOST
request, unless the handler redirects to a page. At that point, you've lost the ability to return data (for example, form validation errors) from the handler. With this change, validation errors could be included in the page props.We do, however, run into an interesting problem here. Suppose you have a
/todos
page like the one from the demo app......and a page that receives the
todos
,values
anderrors
props......then the initial
GET
would be populated withtodos
, but when rendering the page following aPOST
to/todos
, thetodos
prop would be undefined.I think the way to solve this would be to run the
get
handler after thepost
handler has run, and combine the props:It might look odd, but there is some precedent for this — Remix's
useLoaderData
anduseActionData
, the latter of which is only populated after a mutative request.This feels to me like the best solution to #1711.
Combining with
load
In some situations you might still need
load
— for example, in a photo app you might want to delay navigation until you've preloaded the image to avoid flickering. It would be a shame to have to reintroduce all theload
boilerplate at that point.We could get the best of both worlds by feeding the props from the shadow endpoint into
load
:Prerendering
One wrinkle in all this is prerendering. If you've deployed your app as static files, you have most likely lost the ability to do content negotiation, meaning that even though we work around filename conflicts by appending
/index.html
to pages, there's no way to specify you want the JSON version of/blog/my-article
.Also, the MIME type of
prerendered-pages/blog/my-article
would be wrong, since static fileservers typically derive the MIME type from the file extension.One solution: shadow endpoints are accessed via a different URL, like
/_data/blog/my-article.json
(hitting the endpoint directly with anAccept
header would essentially just proxy to the shadow endpoint URL). App authors would need to take care to ensure that they weren't manuallyfetch
ing data from/blog/my-article
in a prerendered app. In the majority of cases, shadow endpoints would eliminate the need forfetch
, so this seems feasible.Alternatives considered
The main alternative idea is #758. A similar version of this has been explored by https://svemix.com. I'm personally more inclined towards shadow endpoints, for several reasons:
The other alternative is to do none of this. I think that would be a mistake — the
load
boilerplate really is unfortunate, but moreover I don't see any other good way to solve #1711.Importance
would make my life easier
Additional Information
No response