How to build a book review app with Svelte, urql and Slash GraphQL - Dgraph Blog

This blog post will teach you how to build a GraphQL app with:

  • urql: a lightweight GraphQL client from Formidable Labs;
  • Svelte: a radical new javascript framework for building user interfaces; and
  • Slash GraphQL: Dgraph’s managed GraphQL backend service.

Slash GraphQL lets you build GraphQL apps without worrying about deployments or translating to GraphQL from other storage domains - it’s just GraphQL, start to finish.

In this post, we’ll be building a basic Svelte-js example app that shows a list of books that users can review. We won’t spend much time on layout or UI look-and-feel, instead, focusing on how easy it is to get those three bits of tech working nicely together. You'll learn some basics of how to use the Svelte javascript framework as well as how to populate a Svelte store from a GraphQL query with urql.

When we are done, a user will see a home screen with a simple display of books, like this.

Each book title and author will be clickable to, for example, see the list of reviews for a book, like this.

From there, a user can submit a new review for a book.

Here are the steps we’ll go through to build the app:

You can find the complete source code for this project here on GitHub.

Create a GraphQL schema

The process for building the app itself is focussed on the Svelte javascript framework and GraphQL queries and mutations in urql — there’s no need to build a GraphQL server because that’s what Slash GraphQL is. However, to get that GraphQL API up and running on Slash GraphQL, we need to design the GraphQL schema for the books app. From the schema, Slash GraphQL builds a running GraphQL API. We’ll first design the schema and then load it into Slash GraphQL in the following section.

We want to store, and display, books and reviews given by different users. This gives us some basic types for the app: books, authors, reviews and users. The Book type contains fields for a name, a genre, an author, and a list of reviews. The Author is its own type, rather than only a String for the author’s name, so that we can store info associated with the author like description and other books written by them - it’s a graph, after all, so the linking of this information could be useful, for example, if we wanted to start building book recommendations into the app. The Review type has text, rating, postedBy, and reviewedBook fields. The review’s postedBy field points to the user who added the review and the User type contains the username and all reviews, postedReviews, posted by the user.

The final GraphQL schema for our app looks like this:

type User {
  username: String! @id
  postedReviews: [Review] @hasInverse(field: postedBy)
}
type Author {
  name: String! @id
  description: String
  books: [Book] @hasInverse(field: author)
}
type Review {
  id: ID!
  text: String!
  rating: Int!
  postedBy: User!
  reviewedBook: Book @hasInverse(field: reviews)
}
type Book {
  id: ID!
  name: String!
  genre: String
  author: Author!
  reviews: [Review]
}

That’s nothing but a GraphQL schema about the types we need for the app; we’ll, with a couple of directives that help Slash GraphQL build the right API. The hasInverse directive helps makes two-way edges, so that adding an edge in one direction automatically also adds one in the inverse direction, hence we have used it for review given by a user and similarly for the author of a book. So we’ll know for example that the postedReviews and postedBy fields are just two directions of the one edge in our graph of books and reviewers.

The @id directive tells Slash GraphQL that username is the unique identifier for users. Those ids come into Slash GraphQL externally when a new user is created, while, for example, a review has an id of type ID, indicating that Slash GraphQL will generate a unique id for every review.

With the schema decided upon, the next step is to create a backend GraphQL API by submitting this schema to Slash GraphQL.

Deploy a GraphQL backend on Slash GraphQL

You'll need an account to create GraphQL backends on Slash GraphQL. There's a generous free tier. If you don't have an account, head over to https://slash.dgraph.io and register for your free account.

Create a GraphQL Deployment

You'll see the empty dashboard screen when you first log into Slash GraphQL.

Just press the “Launch a backend” button. That takes you to a screen to create the backend. You can also check out the “Interactive Tutorial” if you like.

I named my deployment book-review, set it up in AWS US West region, and selected the Free billing plan. Clicking “Launch” spins up the backend infrastructure to serve the GraphQL app. That'll spin for just a few moments, and once you have the green tick, it's live.

While it's spinning up, note down the URL of your GraphQL API. You'll need that to connect it to the Svelte app.

Once the GraphQL backend is live, you give it your GraphQL schema, it serves a GraphQL API - no layers, no translations, no SQL, just GraphQL. So press “Create your Schema”, paste the schema in and press “Deploy”.

That's it — the GraphQL API for the app is up and running and we can now connect it to a Svelte js app.

Setup a Svelte app with urql and GraphQL

For this Svelte example app, we'll create the Svelte app using a template. Navigate to a directory in which you want to set up the app and run:

npx degit sveltejs/template svelte-app

Note: you'll need to have Node.js installed.

That creates a directory svelte-app with a boilerplate Svelte app created. Install the dependencies…

cd svelte-app
npm install

…then start Rollup:

npm run dev

Visit http://localhost:5000/, and you should see the svelte boilerplate app up and running:

We’ll use the @rollup/plugin-replace plugin, which replaces strings in files while bundling. We will use it to replace “process.env.NODE_ENV” with “development”. Update the plugins array in rollup.config.js.

import svelte from "rollup-plugin-svelte";
...
import replace from "@rollup/plugin-replace";
const production = !process.env.ROLLUP_WATCH;
...
export default {
  input: "src/main.js",
  output: {
    sourcemap: true,
    format: "iife",
    name: "app",
    file: "public/build/bundle.js",
  },
  plugins: [
    svelte({
      // enable run-time checks when not in production
      dev: !production,
      // we'll extract any component CSS out into
      // a separate file - better for performance
      css: (css) => {
        css.write("bundle.css");
      },
    }),
    ...
    // If we're building for production (npm run build
    // instead of npm run dev), minify
    production && terser(),
    replace({
      "process.env.NODE_ENV": JSON.stringify("development"),
    }),
  ],
  ...
};
...

Now we add the urql & GraphQL dependencies.

npm install --save @urql/svelte graphql

It’s super simple to setup urql. All that needs to be done is to point the urql client to the Slash GraphQL backend. Open App.svelte, delete the contents, and then add these lines to the script tag.

<script>
  import { initClient } from "@urql/svelte";
  initClient({
    url: "YOUR-SLASH-ENDPOINT"
  });
</script>

That simply initializes the urql client and points it to a GraphQL API. Remember to set the url value to the Slash GraphQL endpoint which we created in the previous step.

If you forgot to copy the URL in the previous step, you can find it in the Slash GraphQL dashboard.

Build the Svelte UI components

The app will have a page for all books, a page for each author's list of books, and a page for each book and its reviews. We'll achieve that by setting up routes for books, reviews, and authors. The Svelte routing library will then render using the right component depending on the URL route.

Set up Svelte routing

We'll use the svelte-routing library to make routing easy for us.

npm install --save svelte-routing

We have to make sure the server is serving “index.html” to all the routes and not only for the “/” route. To do this, change the “package.json” file to add the -s argument to the “start” property.

"start": "sirv public -s"

The app will now serve as a single page application with “index.html” fallback.

Now, we can update App.svelte to have routes. For now, let’s create a single one that points to an empty file components/Home.svelte, we'll add the remainder of the routes as we build the Svelte components for them. In the next section, we'll fill out components/Home.svelte so that it renders a home screen with a list of books.

<script>
  import { Router, Route, Link } from "svelte-routing";
  import Home from "./components/Home.svelte";
  
  export let url = "";
  import { initClient } from "@urql/svelte";
  initClient({
    url: "YOUR-SLASH-ENDPOINT"
  });
</script>
<Router {url}>
  <nav>
    <Link to="/">Home</Link>
  </nav>
  <div>
    <Route path="/">
      <Home />
    </Route>
  </div>
</Router>

The statement

export let url = "";

means that url is a prop with default value "". Adding export in front of a variable declaration in Svelte makes it a prop which receives its value from the parent component. The url variable is then accessible from within the component like {url} which is used by the router to determine which route to call.

Build a Svelte component for the books list

We want the home screen to view a list of books. The ability to give a review to a book and check other reviews for the book can wait till the following sections. Let’s start by creating a basic component that lists all the books. Open Home.svelte and add the following code.

<script>
  import { operationStore, query } from "@urql/svelte";
  const books = operationStore(
    `
   {
  queryBook {
    id
    name
    genre
    author {
      name
    }
  }
}
  `);
  query(books);
</script>
<style>
  .grid-container {
    display: grid;
    grid-template-columns: auto auto auto;
    background-color: #2196f3;
    padding: 10px;
  }
  .grid-item {
    background-color: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(0, 0, 0, 0.8);
    padding: 20px;
    font-size: 30px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    transition: 0.3s;
    margin: 8px;
  }
  .grid-item:hover {
    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
  }
</style>
<main>
  <h1>Welcome to Books Review</h1>F
</main>
<div>
  {#if $books.fetching}
    Loading...
  {:else if $books.error}
    Oh no! {$books.error.message}
  {:else if !$books.data}
    No data
  {:else}
    <div class="grid-container">
      {#each $books.data.queryBook as book}
        <div class="grid-item">
          <div>Name - {book.name}</div>
          <div>Author - {book.author.name}</div>
        </div>
      {/each}
    </div>
  {/if}
</div>

The build of the code renders the book list, but how does the data get in there — urql

Query with urql and the Svelte writeable store

In the code snippet above, the operationStore function creates a Svelte Writable store with a GraphQL query passed to it. The GraphQL query, queryBook, gets the list of all books and their authors. Then, we pass the store to the urql query function, which sends the GraphQL query to the Slash GraphQL backend. Slash GraphQL responds with the book data and Svelte adds it to the store. Now we can use the books store to read the data using a reactive auto-subscription in Svelte. Hence we prefix $books with a dollar symbol, which automatically subscribes us to any changes in the underlying store.

Let’s add a few books by executing a mutation. The screenshot below uses Slash GraphQL’s API Explorer (under the Develop section) to run a GraphQL mutation and add some sample data. You can also open your favorite GraphQL client like Postman, Insomnia, or GraphQL playground.

Here's the mutation to add a list of books and return the data of the books added.

mutation addBooks($data: [AddBookInput!]!) {
  addBook(input: $data) {
    numUids
    book {
      id
      name
    }
  }
}

To run that, you'll need the book data itself. GraphQL has a first-class way to pass in values to a query or mutation, called GraphQL variables. The variables are just JSON data that matches up with the $ variables in the mutation.

{
    "data": [
        {
            "name": "Lean In",
            "author": {
                "name": "Sheryl Sandberg"
            }
        },
        {
            "name": "Mossad",
            "author": {
                "name": "Michael Bar-Zohar"
            }
        },
        {
            "name": "Ikigai",
            "author": {
                "name": "Hector Garcia Puigcerver"
            }
        }
    ]
}

Running the mutation with the given variables will result in a return value like the following (your id values might be different).

{
  "data": {
    "addBook": {
      "numUids": 3,
      "book": [
        {
          "id": "0x2714",
          "name": "Mossad"
        },
        {
          "id": "0x2715",
          "name": "Ikigai"
        },
        {
          "id": "0x2716",
          "name": "Lean In"
        }
      ]
    }
  },
  ...
}

In the Slash GraphQL explorer that will look like the following, where you can see the mutation, the variables, and the result.

You can learn more about the GraphQL API that Slash GraphQL built for the app using GraphQL introspection. Slash GraphqL does that for you (so do other GraphQL IDEs) and you can explore the API right in the browser.

Now, that there's some sample data, run npm run dev and check the browser, and you should see a list of books.

Add links with Svelte routing

Let’s now add a page that can display each author along with all their books. We’ll set up the routes so that passing the author name in the url gets you to details related to a particular author, and we'll add links to the page so that users can explore the book data by clicking on titles, authors names, etc.

Since name is the id for the Author type, that’s the right identifier to use and makes a readable URL. Create an empty file components/Author.svelte which will display author details, and add a route for this in App.svelte.

<script>
import { Router, Route, Link } from "svelte-routing";
import Home from "./components/Home.svelte";
import Author from "./components/Author.svelte";
...
</script>
<Router {url}>
  <nav>
    <Link to="/">Home</Link>
  </nav>
  <div>
    <Route path="/">
      <Home />
    </Route>
    <Route path="author/:name" let:params>
      <Author name={params.name} />
    </Route>
  </div>
</Router>

The author's name gets passed to the Author component, so that component can run a GraphQL query to get the details it needs to render. As with the url variable in App.svelte the author component will set up a variable like:

so that name becomes the prop that lets the Svelte router pass in the author’s name. Now, let’s use that prop in the Author.svelte component to query for the author.

<script>
  import { Router, Route, Link } from "svelte-routing";
  export let name = "";
  import { operationStore, query } from "@urql/svelte";
  const author = operationStore(
    `
   query($name: String!) {
  getAuthor(name: $name) {
    name
    description
    books {
          id
    name
    genre
    author {
      name
    }
    }
  }
}
  `,
    { name }
  );
  query(author);
</script>
<style>
  .grid-container {
    display: grid;
    grid-template-columns: auto auto auto;
    background-color: #2196f3;
    padding: 10px;
  }
  .grid-item {
    background-color: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(0, 0, 0, 0.8);
    padding: 20px;
    font-size: 30px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    transition: 0.3s;
    margin: 8px;
  }
  .grid-item:hover {
    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
  }
</style>
<main>
  <h1>Author</h1>
</main>
<div>
  {#if $author.fetching}
    Loading...
  {:else if $author.error}
    Oh no! {$author.error.message}
  {:else if !$author.data}
    No data
  {:else}
    <div>Name - {$author.data.getAuthor.name}</div>
    <div>Description - {$author.data.getAuthor.description}</div>
    <div class="grid-container">
      {#each $author.data.getAuthor.books as book}
        <div class="grid-item">
          <div>Name - {book.name}</div>
          <div>Author - {book.author.name}</div>
        </div>
      {/each}
    </div>
  {/if}
</div>

The Author component code is pretty similar to the Home component and uses the Svelte store in the same way, with the only difference being that the GraphQL query expects a variable for the author’s name, that’s passed in as the 2nd parameter of operationStore. Again the query function is called to run the GraphQL query using the Slash GraphQL backend and fill the store’s data.

Now, let’s create a page to display a book's details and then link all our pages up. Create an empty file components/Book.svelte, and add a route for this in App.svelte.

<script>
import { Router, Route, Link } from "svelte-routing";
import Home from "./components/Home.svelte";
import Author from "./components/Author.svelte";
import Book from "./components/Book.svelte";
...
</script>
<Router {url}>
  <nav>
    <Link to="/">Home</Link>
  </nav>
  <div>
    <Route path="/">
      <Home />
    </Route>
    <Route path="author/:name" let:params>
      <Author name={params.name} />
    </Route>
    <Route path="book/:id" let:params>
      <Book id={params.id} />
    </Route>
  </div>
</Router>

This time, we pass the book id to the Book component instead of the name. Again, the Book.svelte component will export a prop id so that svelte routing can pass in the id of the book. Now let’s use that prop in the Book component to query for the book and all its reviews.

<script>
  import { Router, Route, Link } from "svelte-routing";
  export let id = "";
  import { operationStore, query } from "@urql/svelte";
  const book = operationStore(
    `
   query($id: ID!) {
  getBook(id: $id) {
    id
    name
    genre
    author {
      name
    }
    reviews {
      id
      postedBy {
        username
      }
      text
      rating
    }
  }
}
  `,
    { id },
   { requestPolicy: 'network-only' } 
  );
  query(book);
</script>
<style>
  .grid-container {
    display: block;
    grid-template-columns: auto auto auto;
    background-color: #2196f3;
    padding: 10px;
  }
  .grid-item {
    background-color: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(0, 0, 0, 0.8);
    padding: 20px;
    font-size: 30px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
    transition: 0.3s;
  }
  .grid-item:hover {
    box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
  }
</style>
<main>
  <h1>Book</h1>
</main>
<div>
  {#if $book.fetching}
    Loading...
  {:else if $book.error}
    Oh no! {$book.error.message}
  {:else if !$book.data}
    No data
  {:else}
    <div class="grid-container">
      <div class="grid-item">
        <div>Name - {$book.data.getBook.name}</div>
        <div>
          Author -
          <Link to={`author/${book.data.getBook.author.name}`}>
            {$book.data.getBook.author.name}
          </Link>
        </div>
      </div>
    </div>
  {/if}
</div>

Similar to the Author component, we pass the query a variable id to get the book.

The code also adds a way to navigate to the Author page by passing the author's name in the Link, so the name is a clickable URL to another route. A similar link can now also be added to the Author component so that we can navigate to the Book component for each book in the author's list. When a user clicks on one of these links, Svelte routing kicks in and loads the right component, passing in, for example, the author's name so the GraphQL query can get the right data.

Run npm run dev (if it isn't already running) and check the browser, you should be able to visit the book and author routes!

The page for a book then looks like this.

While the page for all an author's books looks like this.

Clicking the book and author links navigates between the two pages thanks to Svelte routing.

Use GraphQL mutations in urql to add reviews

So far the components only query data from the Slash GraphQL backend. Adding reviews means running a mutation to update the data. Our app will have a page with a simple form that lets users add reviews.

Let’s add an empty file components/Review.svelte which will be used for collecting a review of books from the user. And, again, add a route for this in App.svelte.

<script>
import { Router, Route, Link } from "svelte-routing";
import Home from "./components/Home.svelte";
import Author from "./components/Author.svelte";
import Book from "./components/Book.svelte";
import Review from "./components/Review.svelte";
...
</script>
<Router {url}>
  <nav>
    <Link to="/">Home</Link>
  </nav>
  <div>
    <Route path="/">
      <Home />
    </Route>
    <Route path="author/:name" let:params>
      <Author name={params.name} />
    </Route>
    <Route path="book/:id" let:params>
      <Book id={params.id} />
    </Route>
    <Route path="review/:id" let:params>
      <Review id={params.id} />
    </Route>
  </div>
</Router>

We pass the id of the book that’s being reviewed to the Review component. This time, that’s not needed for rendering anything on the page (though we could add some book details to the page), instead, it’s only used in the mutation so the review is correctly linked to the book. Now, let’s use that prop in the Review component to submit a review for that book.

<script>
  import { mutation } from "@urql/svelte";
  import { navigate } from "svelte-routing";
  export let id;
  let rating = 2;
  let username = "";
  let review = "";
  const mutateReview = mutation({
    query: `
      mutation ($input: [AddReviewInput!]!) {
        addReview (input: $input) {
          numUids
        }
      }
    `
  });
  function addReview() {
    const reviewObject = [
      {
        text: review,
        rating: rating,
        postedBy: { username: username },
        reviewedBook: { id: id }
      }
    ];
    mutateReview({ input: reviewObject }).then(result => {
      navigate(`/book/${id}`);
    });
  }
</script>
<style>
  .content {
    display: grid;
    grid-template-columns: 20% 80%;
    grid-column-gap: 10px;
    padding-right: 10px;
  }
  .btn {
    text-align: center;
  }
</style>
<main>
  <h1>Review</h1>
</main>
<div class="content">
  <label for="username">Username</label>
  <input bind:value={username} id="username" />
  <label for="rating">Rating</label>
  <input type="number" bind:value={rating} min="0" max="5" id="rating" />
  <label for="text">Text</label>
  <textarea bind:value={review} id="text" />
</div>
<div class="btn">
<button on:click={addReview}>Submit</button>
</div>

Again, the line export let id; is the prop that gets the book id of the book to be reviewed. The other variables rating, username, and review are used by the component to store the review that’s entered into the text fields on the page. The urql mutation function takes the GraphqL mutation and returns an executable function mutateReview, which is triggered when the review is submitted.

That mutateReview, function takes input from the input fields and textarea, that are bound to variables. So when the Submit button is clicked, it calls the addReview function, which forms the input variable to be passed to the execution function mutateReview, which runs the actual mutation on Slash GraphQL. When the response is received after executing the mutation, the app navigates to the Book component for which the review was given. That component will load the book’s data and thus display the existing and new reviews.

Now that we have successfully added a way to give a review, let’s add the link to it from the Book component so that any user can give a review and also display the reviews. Update components/Book.svelte to add a link to submit a new review.

<script>
  ...
</script>
...
<main>
  <h1>Book</h1>
</main>
<div>
  
  ...
  
    <div class="grid-container">
      <div class="grid-item">
        <div>Name - {$book.data.getBook.name}</div>
        <div>
          Author -
          <Link to={`author/${book.data.getBook.author.name}`}>
            {$book.data.getBook.author.name}
          </Link>
          <div>
            {#if $book.data.getBook.reviews.length != 0}
              <div>Reviews - </div>
            {/if}
            {#each $book.data.getBook.reviews as review}
              <div class="grid-item">
                <div>Rating - {review.rating}</div>
                <div>Text - {review.text}</div>
                <div>Username - {review.postedBy.username}</div>
              </div>
            {/each}
          </div>
          <hr>
          <div>
            <Link to={`review/${book.data.getBook.id}`}>Submit Review</Link>
          </div>
        </div>
      </div>
    </div>
  {/if}
</div>

Run npm run dev (if it isn't already running) and check the browser, try adding few reviews!

Conclusion

The blog got you started writing apps in the Svelte javascript framework, with urql and Slash GraphQL. We:

  • started with writing a basic GraphQL schema for our books review app,
  • then got a fully functional GraphQL API from Slash GraphQL by just submitting that schema, and used the Slash GraphQL web UI to add some sample data,
  • then, for the svelte js UI, we set up a boilerplate app and made components for books, authors and reviews, with svelte-routing support, and hooked the whole thing up to our Slash GraphQL API with urql.

It's just an example svelte app, but should get you started. If you want to read more about designing GraphQL schema, check out this blog. To build a real app, you'll also need to learn about Slash GraphQL's support for subscription, authorization, and custom logic. Check the Slash GraphQL docs docs for all the details.

ReferencesTop image: The Ghost Nebula urql client docs: https://formidable.com/open-source/urql/docs/basics/getting-started/#svelte Svelte intro tutorial: https://svelte.dev/tutorial/basics Svelte docs: https://svelte.dev/ Svelte Routing: https://github.com/EmilTholin/svelte-routing Slash GraphQL docs: https://dgraph.io/docs/slash-graphql/introduction/

This is a companion discussion topic for the original entry at https://dgraph.io/blog/post/build-a-svelte-urql-app-with-slash-graphql/