Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server-only load functions #1922

Closed
hdoro opened this issue Jul 16, 2021 · 10 comments
Closed

Server-only load functions #1922

hdoro opened this issue Jul 16, 2021 · 10 comments

Comments

@hdoro
Copy link

hdoro commented Jul 16, 2021

Describe the problem

Thanks for building kit and all the heroic efforts in dealing with this amount of issues!

As the load function runs in both client & server, we always find ourselves creating an extra endpoint for fetching data with server credentials. This is laborious and tedious, leads to duplication of error handling and makes it harder to reason about data flow.

In the example below, notice how /recipes/[slug].svelte and /api/recipes/[slug].js have a lot of duplication:

<!-- /recipes/[slug].svelte -->
<script context="module">
  export const load = async ({ page: { params }, fetch }) => {
    const { slug } = params

    if (!slug || typeof slug !== 'string') {
      return {
        status: 400,
        error: 'Recipe not found',
      }
    }

    // No way to click to open endpoint definition
    const recipe = await (await fetch(`/api/recipes/${slug}`)).json()

    if (!recipe?._id) {
      return {
        status: 404,
        error: 'Recipe not found',
      }
    }

    return {
      props: {
        recipe,
      },
    }
  }
</script>
// /api/recipes/[slug].js
import getFullRecipe from '$lib/queries/getFullRecipe'

export const get = async (request) => {
  const { slug } = request.params

  // Duplicated error handling
  if (!slug || typeof slug !== 'string') {
    return {
      status: 400,
      error: 'Recipe not found',
    }
  }

  const recipe = await getFullRecipe(slug)

  if (!recipe?._id) {
    return {
      status: 404,
      error: 'Recipe not found',
    }
  }

  return {
    status: 200,
    body: recipe,
  }
}

Describe the proposed solution

Ideally, Kit's adapters would automatically transform server-only functions into their own endpoints, without us having to think about it. This would be similar to Next's getServerSideProps.

To annotate a function as server only, we could pass a context="server" tag to the module <script> tag. Here's the example above with this approach:

<script context="module" context="server">
  import getFullRecipe from '$lib/queries/getFullRecipe'

  export const load = async ({ page: { params }, fetch }) => {
    const { slug } = params

    if (!slug || typeof slug !== 'string') {
      return {
        status: 400,
        error: 'Receita não encontrada',
      }
    }

    const recipe = await getFullRecipe(slug)

    if (!recipe?._id) {
      return {
        status: 404,
        error: 'Receita não encontrada',
      }
    }

    return {
      props: {
        recipe,
      },
    }
  }
</script>

Single file, ~50% code reduction (29 LOC vs. 55 in the example above), one less file to manage ✨

Alternatives considered

I'm considering creating my own abstractions to make this slightly easier to reason about. For example, I could condense all data-fetching server endpoints into a single get function that takes whatever query I need to perform and returns the data accordingly.

The issue here is that it still wouldn't be as simple as a native solution and we'd be opening up a lot of dissonance between different codebases.

I understand this is potentially a very hard issue to tackle, but if doable it feels like a pre-v1 feature to make dynamic routes easier to build & onboard new people.

Importance

would make my life easier

Additional Information

Sorry if I double posted this - searched hard through issues and couldn't find another. So many issues coming in, thanks for putting in the work to deal with all this pressure 🙏

@dummdidumm
Copy link
Member

Sounds related to sveltejs/rfcs#31 (RFC talks about Sapper, but the same could be applied to SvelteKit)

@hdoro
Copy link
Author

hdoro commented Jul 16, 2021

It's exactly the same use case as this RFC, Simon! Thanks for sharing :D

I wonder if that will circle back to SvelteKit, the RFC seems to be quite stale 🤔

@rmunn
Copy link
Contributor

rmunn commented Jul 27, 2021

I could see potential issues with Vite seeing import statements in a .svelte file and wanting to treat them as client code, rather than server code; e.g. #1570 and the like. I don't think it would be reasonable to expect Vite's heuristics to learn the difference between <script context="module"> and <script context="module" context="server">; that seems like it would be the job of Svelte-Kit to detect that case and adjust the Vite config in svelte.config.js to treat those modules as server-side code. I don't know exactly how that could be handled during the build process, but perhaps people more familiar with Svelte-Kit's internals will have an idea.

@jarda-svoboda
Copy link

jarda-svoboda commented Dec 28, 2021

Or maybe something like this might work as well..

The idea is to pass the data from server-side load method to client-side load similar way like SvelteKit's fetch does, but using some parameter (e.g. data). It could work similar way like stuff parameter for __layouts, but in this case between server and client...

UPDATE:
Oh, i previously didn’t notice, that stuff should work for all subsequent load methods, so my code should probably work with the stuff parameter instead of data, right? (or subsequent does not mean server-client in this case?)

From the docs:

For the root __layout.svelte component, it is equal to {}, but if that component's load function returns an object with a stuff property, it will be available to subsequent load functions.

<script context="module">
  import getFullRecipe from '$lib/queries/getFullRecipe'

  export const load = async ({ page: { params }, fetch, data }) => {

    const { slug } = params;

    if (!slug || typeof slug !== 'string') {
      return {
        status: 400,
        error: 'Receita não encontrada',
      }
    }

    let { recipe } = data;

    if (!recipe) {
      recipe = await getFullRecipe(slug)
    }

    if (!recipe?._id) {
      return {
        data: { recipe: {} },
        status: 404,
        error: 'Receita não encontrada',
      }
    }

    return {
      data: {
        recipe,
      },
      props: {
        recipe,
      },
    }
  }
</script>

@ariesclark
Copy link

A year later, what's the status on this? I haven't seen any progress on the proposed RFC or other issues.

@aral
Copy link

aral commented Jan 24, 2022

@RubyBB Someone please correct me if I’m wrong but I believe this is incompatible with the “serverless” route SvelteKit’s taken.

I’m working on a tightly coupled Node.js server that’s basically designed around this pattern with NodeKit. It has a much narrower focus and is at a VERY early stage right now, but in case it helps, check out https://github.com/small-tech/nodekit

For example, this what you can do today (but, again, NodeKit is nowhere ready for actual use yet):

<data>
  if (db.greetings === undefined) {
    db.greetings = { count: 1 }
  }

  export default () => {
    return {count: db.greetings.count++}
  }
</data>

<script>
  export let data
</script>

<h1>Hello, world!</h1>

<p>I’ve greeted you {data.count} times.</p>

(That’s a fully functional example. Clone the repo, run ./install, pop the above code into a file called index.page in some folder and run nodekit in the folder and hit https://localhost to have a play by refreshing the page. Restart the server, note the count is retained. See the greetings.js file in the .db folder for how.) ;)

@kevmodrome
Copy link

A year later, what's the status on this? I haven't seen any progress on the proposed RFC or other issues.

I'd like to suggest taking a look at Svemix in the meantime: https://svemix.com

@kevmodrome
Copy link

@RubyBB Someone please correct me if I’m wrong but I believe this is incompatible with the “serverless” route SvelteKit’s taken.

I’m working on a tightly coupled Node.js server that’s basically designed around this pattern with NodeKit. It has a much narrower focus and is at a VERY early stage right now, but in case it helps, check out https://github.com/small-tech/nodekit

For example, this what you can do today (but, again, NodeKit is nowhere ready for actual use yet):

<data>
  if (db.greetings === undefined) {
    db.greetings = { count: 1 }
  }

  export default () => {
    return {count: db.greetings.count++}
  }
</data>

<script>
  export let data
</script>

<h1>Hello, world!</h1>

<p>I’ve greeted you {data.count} times.</p>

(That’s a fully functional example. Clone the repo, run ./install, pop the above code into a file called index.page in some folder and run nodekit in the folder and hit https://localhost to have a play by refreshing the page. Restart the server, note the count is retained. See the greetings.js file in the .db folder for how.) ;)

This looks really cool! You should add it to the newsletter that's coming up in a bit! 👍

@aral
Copy link

aral commented Jan 24, 2022

@kevmodrome Thanks, Kevin, but might be better to hold off a bit. It’s very early, I’d hate to get people too excited while it’s evolving rapidly. If anything, maybe with a huge warning that it’s not ready for use yet but might be fun to start playing with :)

@Rich-Harris
Copy link
Member

Closing in favour of #3532

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants