4

Effective Routing Techniques

We’ve spent a lot of time covering background information. I know those topics aren’t always the most exciting, but now that we’ve covered them, we can get to the real fun. Up until now, we’ve only briefly touched on adding new routes by creating a directory inside src/routes/ with the desired route name and adding a +page.svelte file inside of it. We also briefly looked into creating server pages. But, of course, routing isn’t always so simple. How do we build out an application programming interface (API)? How do we create a consistent user interface (UI) throughout our application without duplicating styles on each page? What happens when our application throws an error?

In this chapter, we will answer some of those questions by discussing some core points about routing within the context of SvelteKit. First, we’ll look at how we can create new pages with dynamic content. Then, we’ll take a closer look at how the +page.server.js files work. We’ll then show how to go about creating API endpoints that can accept various types of HTTP requests. And finally, we’ll cover how to build a consistent UI throughout an application using layouts.

In this chapter, we’ll cover the following topics:

  • Creating Dynamic Pages
  • Creating Server Pages
  • Creating API Endpoints
  • Creating Layouts

By the end of this chapter, you should have a comfortable understanding of routing concepts for SvelteKit’s file-based routing mechanism.

Technical requirements

The complete code for this chapter is available on GitHub at: https://github.com/PacktPublishing/SvelteKit-Up-and-Running/tree/main/chapters/chapter04.

Creating Dynamic Pages

In previous chapters, we’ve covered the process of creating a new page. To refresh your memory, it is as simple as creating a new directory inside src/routes/ with the desired route name. Inside that directory, we create +page.svelte, which is simply a Svelte component that is then rendered as the page shown in the browser. An embarrassingly simple About page might look like this:

src/routes/about/+page.svelte

<div class='wrapper'>
  <h1>About</h1>
  <p>
    Lorem ipsum dolor sit amet...
  </p>
</div>

This example illustrates just how simple adding a new page is. In it, we see a div, an h1 title tag, and a paragraph p tag with the lorem ipsum sample text. Of course, in a real-world scenario, it would have much more content as well as some styles. This example exists only to show how simple it is to add a new, static page.

But what if you needed to create a page where you didn’t know what the content was? What if we wanted to create a page with a dynamic URL? For instance, when viewing news articles online, users can typically share a link directly to the article. This means each article has its own unique URL. If we wanted to create a single template that showed articles pulled from a database, we would need to find a way to manage the URL to each as well.

In instances such as these, SvelteKit’s file-based routing mechanism has a special syntax to use. When creating the template showing the content, we create it in a directory with the dynamic portion of the route surrounded by square brackets ([ ]). The name given inside the brackets will then become a parameter that will allow us to load data dynamically. To make this parameter optional, use double square brackets ([[ ]]).

That may sound confusing at first, so let’s take a look at an example showcasing how you might manage news articles or blog posts. We’ll need to create a few files within this example. Instead of connecting to an actual database, we’ll use a JSON file to store some sample data and pull directly from that:

src/lib/articles.json

{
  "0": {
    "title": "First Post",
    "slug": "first-post",
    "content": "Lorem ipsum dolor…"
  },
  "1": {
    "title": "Effective Routing Techniques",
    "slug": "effective-routing-techniques",
    "content": "Lorem ipsum dolor…"
  }
}

This file is essentially a single object containing two objects, each of which has the title, slug, and content properties. You can tweak this file however you like or add as many entries as you’d like to see. Its purpose is to act as a placeholder database to illustrate the following example.

Next, a news page usually has a landing page where users can scroll and view the most recent articles. In accordance with established practices, we’ll create a +page.server.js file to load our data and make it available to the +page.svelte template that will function as the page rendering the data:

src/routes/news/+page.server.js

import json from '$lib/articles.json';
export function load() {
  return { json };
}

Because it doesn’t contain any environment variables or secrets or make a call to a real database, this file could just as well be a +page.js file. It works only to load data from the JSON example file. Essentially, it imports that file and then returns it as an object in the exported load() function, making it available to the Svelte template shown next. In reality, something so trivial could also be done in the <script> tag of the Svelte template but remember that this example aims to function as a stand-in for a real database.

Now that we’ve loaded our articles, we’ll need somewhere to show them, so let’s do that now and create a landing page for our news. For the sake of brevity, styles have been omitted:

src/routes/news/+page.svelte

<script>
  export let data;
</script>
<h1>
  News
</h1>
<ul>
  {#each Object.entries(data.json) as [key, value]}
    <li>
      <a href='/news/{value.slug}'>{value.title}</a>
    </li>
  {/each}
</ul>

This Svelte component exports the data variable, giving us access to the data we previously returned from our faked database in load(). It then adds an h1 title tag, followed by an unordered list of each of the entries from our fake data. After which, it makes use of the {#each} Svelte template syntax to iterate over each of the entries in the array returned from Object.entries(data.json). For each of the entries (our two sample objects), we surround them with list item tags and show the title property inside a <a> tag linking to the article via the slug property.

Next, we’ll need to create a page to show the article content but keep in mind we want the route to have a parameter within it, so we’ll use the [ ] square brackets to surround the article slug:

src/routes/news/[slug]/+page.svelte

<script>
  export let data;
</script>
<h1>News</h1>
<h2>{data.title}</h2>
<div class='content'>
  {data.content}
</div>
<a href='/news'>Back to news</a>

This file exports the data variable so that we may access information about our article. It then shows News inside a <h1> title tag, followed by a <h2> title tag with the article title, a div containing the article content, and a link back to the news page. All of the article information is loaded by making another call to our database in the next file:

src/routes/news/[slug]/+page.server.js

import json from '$lib/articles.json';
import { error } from '@sveltejs/kit';
export function load({ params }) {
  let found = {};
  Object.keys(json).forEach((id) => {
    if(json[id].slug === params.slug) {
      found = json[id];
    }
  });
  if(Object.keys(found).length !== 0) {
    return found;
  }
  throw error(404, 'Whoops! That article wasn't found!');
}

Just as before, we’ve imported our fake database to access the full article content. This code also imports the error module from SvelteKit, which will come in handy later on. We’ve then exported the load() function so that we can return the loaded data to the rendered page. Inside the load() function, the code initializes an empty variable labeled found, and then begins to iterate over each object inside of the JSON data. In the loop, it checks whether any of the slugs in our data match the given slug in the URL. If a match is found, it is then assigned to the found variable. After finishing the loop, we check that found is not an empty object. If it is not empty, we return an object containing the found variable. If it is empty, we throw a 404 Not Found error.

Upon opening your development site in your browser and navigating to /news, you should see two article titles listed. When clicked, they will redirect users to the respective article. This example illustrates routing with parameters in a simple way that works for the most part. But what do we do about the times when it doesn’t work? Have you tried navigating to an article that doesn’t exist yet? Go ahead and try it now; I’ll wait right here:

Figure 4.1 – A generic error page is shown when throwing an error in SvelteKit

The article doesn’t exist, and the user is shown a generic error page. If we didn’t throw an error at all, the page would be rendered showing the undefined values. Instead, we should show the user a proper error page. Just as SvelteKit provides us with the +page.svelte, +page.js, and +page.server.js files, we can also create an +error.svelte template, which can be used when errors are thrown from the application. By specifying it in the src/routes/news/[slug]/ directory, the error page template will be localized to that particular route. If we wanted to build a generic error page to be used across the entire application, we could do so by placing an +error.svelte template at the root route of the application (src/routes/+error.svelte). Let’s create a template in src/routes/news/[slug]/ so users aren’t confused by our lack of clear communication:

src/routes/news/[slug]/+error.svelte

<script>
  import { page } from '$app/stores';
</script>
<h1>News</h1>
<h2>{$page.status}</h2>
<div class='content'>
  {$page.error.message}
</div>
<style>
  * {
    font-family: sans-serif;
  }
  h1, h2, .content {
    text-align: center;
    color: #358eaa;
  }
</style>

For this error template, we import the page store module to access information about this particular request. This module, as well as many others, is available to use throughout the application. As this one makes use of Svelte’s stores, we can access the values it contains about the page by prefacing it with a dollar sign ($). The rest of this template is fairly straightforward. It includes the <h1> title tag labeled News, followed by the status code and error message we passed when we threw our error in +page.server.js. Some styles have been included to show how this template is different from the default template shown throughout SvelteKit. Compare Figure 4.1 of the generic SvelteKit error template to Figure 4.2, our custom version:

Figure 4.2 – A customized error page template

By now, you should feel comfortable creating essential routes for your application, whether they are static or dynamic. You should also be able to show error pages based on the route. There will be more about advanced routing later on, but for now, let’s take a closer look at how the +page.server.js files work.

Creating Server Pages

In previous examples, we’ve used the +page.js and +page.server.js files for loading data. Often, they can be used interchangeably, but when is the best time to use which? In this section, we’ll break down some of the differences between the two and also discuss various features available in the +page.server.js files. We’ll break it down into these topics:

  • load()
  • Page options
  • Actions

load()

As we’ve seen in previous examples, data can be loaded into a +page.svelte component by exporting the data property on that page. Both +page.js and +page.server.js can then be used for loading data to that page template as they can both export a load() function. When to use which file depends on how you plan to load that data. When run in a +page.js file, load() will run on both the client and the server. It is recommended to load data here if you are able, as SvelteKit can manage grabbing data from calls with fetch(). This becomes particularly useful when preloading data (anticipating what the user may do and starting the process milliseconds before they actually do it).

However, there are times this isn’t possible. For instance, if you need to make a call to an API that requires authentication or a database, you likely don’t want your connection secrets exposed to the client. That would allow anyone with access to your application to then download your secrets and make requests to said API or database on your behalf. In these cases, you’ll need to store the secrets on your server. And since those secrets are on your server, you’ll need access to the server’s filesystem. The server will need to make the calls to obtain the appropriate data and pass that data to the client. In instances such as these, it is best to use the +page.server.js files for data loading requirements.

Page options

+page.js and +page.server.js files are not just used for loading data. They can also export various options specific to their sibling page. Some options allow you to configure functionality related to the rendering of pages. These particular options are Boolean values, which means they can be enabled by setting them to true or disabled by setting them to false. They are the following:

  • prerender
  • ssr
  • csr

prerender

While prerendering can be customized in a svelte.config.js project, you may find yourself needing to explicitly enable or disable it on a per-page basis. To enable it on a page, you can set export const prerender = true;. Conversely, setting its value to false will disable prerendering for that page. When to prerender a page should be determined by whether or not the HTML content of the page is static. If the HTML shown on the page should be the same no matter who is viewing it, then a page is considered safe to prerender. By prerendering a page, the HTML will be generated at build time, and static HTML will be shipped to the client for each request to that particular route. This results in faster load times for end users, which makes for a better experience.

Server-side rendering

Server-side rendering (SSR) can be a powerful tool in your arsenal to speed up the user experience. Instead of forcing the client to load a library that manages all of the rendering of content, the server will take over that responsibility so that it may ship static HTML to the client, which will then hydrate the page with data. To enable SSR for a particular route, set export const ssr = true; in the +page.js file or +page.server.js. SvelteKit enables this option by default, so if you need to change it, you’ll likely find yourself disabling it by setting the value to false.

Client Side Rendering

Instead of rendering the page on the server, and sending that to the client, enabling client-side rendering (CSR) will force the client to handle the workload of rendering. This can be useful when creating a single-page application (SPA), as routing will then be managed on the client device. Be warned that if you have disabled both SSR and CSR, nothing on the page will be rendered. Instead, you’ll see an empty shell of a page. To disable CSR, set export const csr = false; just as you would with other available page options.

You’ll likely find these rendering-related options useful should you find yourself building an SPA, a static HTML site, or if you’re attempting to render static content on the client end.

Actions

Because +page.js files are also run in the client, they cannot export actions. Actions allow you to receive data submitted by the form elements sent via the POST HTTP method. We saw an example of this in Chapter 3, where we discussed the FormData API compatibility with SvelteKit. In that example, we exported a default action. A default action will be triggered by submitting a form element that has not specified an action property. However, we are not limited to only default actions, as SvelteKit also enables us to create named actions. Named actions will all work on the same route but are differentiated from each other by providing the route followed by the action name in a query string. A default action may not exist alongside named actions, so you must remove it or change its name and set the action property on the form elements that utilize it.

Building from our previous example, let’s look at how we might implement a few more actions related to comments online. Some additional functionality we may want to create would be allowing users to star a comment or reply. Since we’ll be adding more named actions, we’ll change the default action to create:

src/routes/comment/+page.server.js

export const actions = {
  create: async (event) => {
    const form = await event.request.formData();
    …
  },
  star: async () => {
    return {
      status: true,
      msg: 'You starred this comment!'
    }
  },
  reply: async () => {
    return {
      status: true,
      msg: 'You replied!'
    }
  }
}

Notice how the default action was changed to create. The code within create has been omitted as it has not changed since our last example. We’ve also added the star and reply actions. For now, they don’t do much except return an object that will output our message, showcasing that they are both called when the respective button is clicked. In a real-world scenario, these would likely make calls to a database, increasing the “star count,” or saving the reply comment content and a unique identifier of the comment being replied to.

As for the form itself, we could create separate forms and specify the POST method as well as the action property for each new feature. However, a more intuitive user experience would consolidate the features and keep them all in one cohesive component. Instead of creating multiple forms, we’ll create a button for the new features and specify a formaction property for each. Doing this will keep the HTTP method specified in the parent form but allow sending the requests to different actions based on the button clicked:

src/routes/comment/+page.svelte

<script>
  import { enhance } from '$app/forms';
  export let form;
</script>
<div class='wrap'>
  {#if form && form.status === true}
    <p>{form.msg}</p>
  {/if}
  <form method='POST' action='?/create' use:enhance>
    <label>
      Comment
      <input name="comment" type="text">
    </label>
    <button>Submit</button>
    <button formaction='?/star'>Star</button>
    <button formaction='?/reply'>Reply</button>
  </form>
</div>
<style>
…
</style>

The first change to notice from our previous encounter with this example is that we’ve added import { enhance } from '$app/forms';. This addition will allow us to progressively enhance our form with JavaScript. The page will then not need to be reloaded after each form submission. This module is utilized in the <form> element further down with the Svelte use: directive. Try running the example without it and observe how the URL will now contain query strings based on which button is clicked.

Speaking of buttons, we’ve added two in this example. Each has the formaction property set, which allows us to specify which of our named actions we would like called from +page.server.js. Take note that we must call these actions by specifying a query parameter followed by a / character. We’ve also set the form action to ?/create. Since we have exported named actions, we can no longer have an action named default and must specify the action to be called on the form element. If we wanted to call an action located at another route, we could do so easily by setting formaction to the desired route name followed by ?/, and the action name.

You should now be confident in knowing when to use +page.server.js over +page.js, how to customize the rendering of pages, and how you can easily accept data from the form elements. In the next section, we’ll cover how you can create API endpoints that accept more than just POST requests.

API Endpoints

We’ve covered the +page.svelte, +page.js, and +page.server.js files but we have yet to discuss +server.js files. These files enable us to accept more than just POST requests. As web application developers, we may be expected to support various platforms. Having an API simplifies the transmission of data between our server and these other platforms. Many APIs can accept GET and POST requests as well as PUT, PATCH, DELETE, or OPTIONS.

A +server.js file creates an API endpoint by exporting a function with the name of the HTTP request method you would like for it to accept. The functions exported will take a SvelteKit-specific RequestEvent parameter and return a Response object. As an example, we can create a simple endpoint that would allow us to create posts for a blog. This could be useful if we used a mobile app to write and post from. Note that a +server.js file should not exist alongside page files as it is intended to handle all HTTP request types:

src/routes/api/post/+server.js

import { json } from '@sveltejs/kit';
export function POST({ request }) {
  // save post to DB
  console.log(request);
  return json({
    status: true,
    method: request.method
  });
}
export function GET({ request }) {
  // retrieve post from DB
  console.log(request);
  return json({
    status: true,
    method: request.method
  });
}

This file imports the json module from the @sveltejs/kit package, which is useful for sending JSON-formatted Response objects. We then export functions named for both POST and GET methods, each of which only outputs the Request object to the console and then returns a Response JSON. If we were so inclined, we could also export functions for other HTTP verbs such as PUT, PATCH, or DELETE.

You should demo this example by navigating to the api/post/ route in your browser. After opening the page, observe the rest of the properties available in the Request object output to your development server. If you don’t understand what you’re looking at, that’s okay because we’ll look into it more in the next chapter. Back in your browser, open the Network tab in your developer tools, select the GET request, right-click it, select Edit and Resend, and change the request method to POST. Once done, send it and view the output. You should see the object with the method property set to POST returned in a JSON formatted object. If your browser doesn’t allow you to edit requests, proxy tools such as OWASP ZAP, Burp Suite, Postman, or Telerik Fiddler will let you customize HTTP requests. See the resources at the end of this chapter for links.

Now that you know how to go about creating your very own API, let’s look at how you can unify the user experience of your application with a layout.

Creating Layouts

We've covered a lot so far in this chapter, but we’ve still only added styles and markup to each specific page. This is repetitive and not a practical use of our time. To reduce repetition, we can utilize layouts. A +layout.svelte component can unify the user experience by leveraging Svelte’s <slot> directive. The layout file will nest any sibling page components and child routes within itself, allowing us to show persistent markup across the application. Just like +page.svelte, we can include a +layout.svelte component at any level in our route hierarchy, allowing the nesting of layouts within layouts. Because each layout is also a Svelte component, the styles will be localized to that particular component and will not cascade to those nested within. Let’s look at how we might use layouts to create a consistent layout and navigation menu for our existing code:

src/routes/+layout.svelte

<script>
  import Nav from '$lib/Nav.svelte';
</script>
<div class='wrapper'>
  <div class='nav'>
    <Nav />
  </div>
  <div class='content'>
    <slot />
  </div>
  <div class='footer'>
    This is my footer
  </div>
</div>
<style>
  .wrapper {
    min-height: 100vh;
    display: grid;
    grid-template-rows: auto 1fr auto;
  }
  .footer {
    text-align: center;
    margin: 20px 0;
  }
</style>

Because this +layout.svelte component is at the root level of our routes, it will be applied across all child routes. The very first thing our component does is import our custom navigation component (shown next). Secondly, it creates the markup that will house the rest of our application, including this file’s sibling +page.svelte. Its markup consists of several <div> elements with varying class names indicating functionality. The .wrapper <div> element wraps all others so that we may apply the sticky footer styles found in the <style> section of this component. The div with the .nav class contains our custom Nav component, the .content div contains our Svelte <slot> directive, and .footer is where we would put our site footer information. Now let’s take a look at the custom Nav component we imported in our root layout:

src/lib/Nav.svelte

<nav>
  <ul>
    <li><a href='/'>Home</a></li>
    <li><a href='/news'>News</a></li>
    <li><a href='/fetch'>Fetch</a></li>
    <li><a href='/comment'>Comment</a></li>
    <li><a href='/about'>About</a></li>
    <li><a href='/api/post'>API</a></li>
  </ul>
</nav>
<style>
  ul {
    list-style: none;
    text-align: center;
  }
  ul li {
    display: inline-block;
    padding: 0;
    margin: 1em .75em;
  }
</style>

This component merely contains HTML with links to all of the routes we’ve already created and some rudimentary styling. It consists of the <a> elements with the href properties set to the relative routes we’ve created up to this point. They are all nested within list items of an unordered list, contained within a <nav> element. Again, this example is overly simple, but for our purposes, it works. Now, you can add any new routes we create later on in the book to the navigation menu so they may be easily accessed when testing in the browser.

Relative routes

Notice how the routes provided are relative and are not prefaced by a domain name. If we were to deploy our production application to a subdirectory; rather than the root folder of our domain, these routes would fail. Instead, we can set the base path of our application in svelte.config.js. Specifically, we’d set config.kit.paths.base to our subdirectory path, starting with a /. Then in components and routes, we could use import { base } from $app/paths and preface all routes with {base}/. In this way, our application would know it exists within a subdirectory. Try doing it in your development project and observe how Vite and SvelteKit automatically serve the project from that directory!

To practice the concepts surrounding layouts further, try creating src/routes/news/[slug]/+layout.svelte to give the articles a consistent appearance. Just as we saw with the +page.svelte files, the +layout.svelte files can be accompanied by the +layout.js or +layout.server.js files. Their functionality is identical to their page counterparts, but the data returned from them will be available in +layout.svelte as well as any +page.svelte pages that exist in parallel. Page options can also be set in layout files, and those options will “trickle down” to nested components.

With the information provided, you should now have the skills necessary to produce consistent and robust UIs for your SvelteKit applications. Layouts come in handy when creating all sorts of UI elements, but especially those that must remain consistent across various portions of the app.

Summary

In this chapter, we covered how to create static and dynamic routes as well as manage custom error templates for those routes. We also saw how developers can accept data submitted via the <form> elements with multiple named actions that can be called from a single form. We learned how to leverage SvelteKit’s routing mechanism to build out an API, which is particularly useful when an application needs to be accessed from platforms other than web browsers. We then unified the UI of our application with layouts. With those layouts, we saw how they can be leveraged to keep navigation elements in a predictable location across the app. That’s a lot of information to absorb in these few pages so we’ll take a closer look at some of these concepts in the next few chapters.

In the next chapter, we will learn more about managing the data we’re loading onto our pages. We’ll also cover more advanced methods for loading that data.

Resources

HTTP Proxy/Sending Tools:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.194.84