Tutorials

How to Use Getters to Retrieve Data

Learn how to create and use getters to retrieve data from databases, APIs, and other sources in your Joystick app.

This tutorial was written by AI

The examples and explanations have been reviewed for accuracy. If you find an error, please let us know.

In this tutorial, you'll learn how to use Joystick's getter system to retrieve data from your database or external APIs. Getters are server-side functions that fetch data and make it available to your components.

What You'll Build

By the end of this tutorial, you'll have:

  • Created a getter to fetch book data from a database
  • Used the getter in a component to display the data
  • Implemented input validation and authorization
  • Added error handling and loading states

Prerequisites

Before starting, make sure you have:

Step 1: Create Your First Getter

Getters are defined in the /api folder. Let's create a getter to fetch books from a database:

api/books/getters.js

const getters = {
  books: {
    get: () => {
      return process.databases.mongodb.collection('books').find().toArray();
    }
  }
};

export default getters;

This basic getter:

  • Defines a getter named books
  • Uses the get() method to define the retrieval logic
  • Accesses the MongoDB database via process.databases.mongodb
  • Returns all books from the books collection

Step 2: Register the Getter

To make your getter available, you need to register it in your API schema:

api/index.js

import book_getters from './books/getters.js';

const api = {
  getters: {
    ...book_getters,
  },
  setters: {
    // Your setters will go here
  }
};

export default api;

Step 3: Use the Getter in a Component

Now let's create a component that uses our getter to display books. First, let's use the data() function for server-side rendering:

ui/components/book_list/index.js

import joystick from '@joystick.js/ui';

const BookList = joystick.component({
  data: async (api) => {
    return {
      books: await api.get('books')
    };
  },
  css: `
    .book-list {
      max-width: 800px;
      margin: 0 auto;
      padding: 2rem;
    }

    .book-list h2 {
      color: #333;
      margin-bottom: 1.5rem;
      font-size: 1.8rem;
    }

    .book-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 1.5rem;
    }

    .book-card {
      background: white;
      border-radius: 8px;
      padding: 1.5rem;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      transition: transform 0.2s, box-shadow 0.2s;
    }

    .book-card:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
    }

    .book-title {
      font-size: 1.2rem;
      font-weight: 600;
      color: #333;
      margin-bottom: 0.5rem;
    }

    .book-author {
      color: #666;
      margin-bottom: 0.5rem;
    }

    .book-year {
      color: #888;
      font-size: 0.9rem;
    }
  `,
  render: ({ data, each }) => {
    return `
      <div class="book-list">
        <h2>Our Book Collection</h2>
        <div class="book-grid">
          ${each(data?.books, (book) => `
            <div class="book-card">
              <div class="book-title">${book.title}</div>
              <div class="book-author">by ${book.author}</div>
              <div class="book-year">${book.year}</div>
            </div>
          `)}
        </div>
      </div>
    `;
  },
});

export default BookList;

The data() function:

  • Runs on the server during server-side rendering
  • Receives an api object with methods to call your getters
  • Returns data that becomes available as data in the render() function

Step 4: Add Input Validation

Let's enhance our getter to accept input parameters and validate them:

api/books/getters.js

const getters = {
  books: {
    input: {
      category: {
        type: 'string',
        required: false,
      },
      limit: {
        type: 'integer',
        required: false,
        max: 100,
      }
    },
    get: (input = {}) => {
      const query = {};
      
      if (input.category) {
        query.category = input.category;
      }

      const limit = input.limit || 20;

      return process.databases.mongodb
        .collection('books')
        .find(query)
        .limit(limit)
        .toArray();
    }
  }
};

export default getters;

Now update the component to use the input parameters:

ui/components/book_list/index.js

import joystick from '@joystick.js/ui';

const BookList = joystick.component({
  data: async (api, req, input) => {
    return {
      books: await api.get('books', {
        input: {
          category: input?.category,
          limit: input?.limit || 10
        }
      })
    };
  },
  state: {
    selectedCategory: 'all',
    loading: false,
  },
  events: {
    'change .category-select': async (event, instance) => {
      const category = event.target.value;
      
      instance.set_state({ 
        selectedCategory: category,
        loading: true 
      });

      // Refetch data with new category
      await instance.data.refetch({
        category: category === 'all' ? null : category,
        limit: 10
      });

      instance.set_state({ loading: false });
    },
  },
  css: `
    .book-list {
      max-width: 800px;
      margin: 0 auto;
      padding: 2rem;
    }

    .book-list h2 {
      color: #333;
      margin-bottom: 1rem;
      font-size: 1.8rem;
    }

    .filters {
      margin-bottom: 2rem;
    }

    .category-select {
      padding: 0.5rem;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 1rem;
    }

    .loading {
      text-align: center;
      padding: 2rem;
      color: #666;
    }

    .book-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 1.5rem;
    }

    .book-card {
      background: white;
      border-radius: 8px;
      padding: 1.5rem;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      transition: transform 0.2s, box-shadow 0.2s;
    }

    .book-card:hover {
      transform: translateY(-2px);
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
    }

    .book-title {
      font-size: 1.2rem;
      font-weight: 600;
      color: #333;
      margin-bottom: 0.5rem;
    }

    .book-author {
      color: #666;
      margin-bottom: 0.5rem;
    }

    .book-category {
      background: #e3f2fd;
      color: #1976d2;
      padding: 0.25rem 0.5rem;
      border-radius: 12px;
      font-size: 0.8rem;
      display: inline-block;
      margin-bottom: 0.5rem;
    }

    .book-year {
      color: #888;
      font-size: 0.9rem;
    }
  `,
  render: ({ data, state, each, when }) => {
    return `
      <div class="book-list">
        <h2>Our Book Collection</h2>
        
        <div class="filters">
          <select class="category-select">
            <option value="all" ${state.selectedCategory === 'all' ? 'selected' : ''}>All Categories</option>
            <option value="fiction" ${state.selectedCategory === 'fiction' ? 'selected' : ''}>Fiction</option>
            <option value="non-fiction" ${state.selectedCategory === 'non-fiction' ? 'selected' : ''}>Non-Fiction</option>
            <option value="science" ${state.selectedCategory === 'science' ? 'selected' : ''}>Science</option>
            <option value="history" ${state.selectedCategory === 'history' ? 'selected' : ''}>History</option>
          </select>
        </div>

        ${when(state.loading, `
          <div class="loading">Loading books...</div>
        `)}

        ${when(!state.loading, `
          <div class="book-grid">
            ${each(data?.books, (book) => `
              <div class="book-card">
                <div class="book-title">${book.title}</div>
                <div class="book-author">by ${book.author}</div>
                ${book.category ? `<div class="book-category">${book.category}</div>` : ''}
                <div class="book-year">${book.year}</div>
              </div>
            `)}
          </div>
        `)}
      </div>
    `;
  },
});

export default BookList;

Step 5: Add Authorization

Let's add authorization to ensure only logged-in users can access the books:

api/books/getters.js

const getters = {
  books: {
    authorized: (input = {}, context = {}) => {
      return !!context?.user;
    },
    input: {
      category: {
        type: 'string',
        required: false,
      },
      limit: {
        type: 'integer',
        required: false,
        max: 100,
      }
    },
    get: (input = {}, context = {}) => {
      const query = {};
      
      if (input.category) {
        query.category = input.category;
      }

      // Only show books available to this user's region
      if (context.user?.region) {
        query.availableRegions = { $in: [context.user.region] };
      }

      const limit = input.limit || 20;

      return process.databases.mongodb
        .collection('books')
        .find(query)
        .limit(limit)
        .toArray();
    }
  }
};

export default getters;

Step 6: Call Getters Directly from Components

You can also call getters directly from component event handlers using the get() method:

ui/components/book_search/index.js

import joystick, { get } from '@joystick.js/ui';

const BookSearch = joystick.component({
  state: {
    searchResults: [],
    loading: false,
    searchTerm: '',
  },
  events: {
    'submit .search-form': async (event, instance) => {
      event.preventDefault();
      
      const searchTerm = event.target.search.value;
      
      if (!searchTerm.trim()) return;

      instance.set_state({ 
        loading: true,
        searchTerm 
      });

      try {
        const books = await get('books', {
          input: {
            category: 'fiction',
            limit: 5
          }
        });

        instance.set_state({ 
          searchResults: books,
          loading: false 
        });
      } catch (error) {
        console.error('Search failed:', error);
        instance.set_state({ 
          loading: false,
          searchResults: []
        });
      }
    },
  },
  css: `
    .book-search {
      max-width: 600px;
      margin: 2rem auto;
      padding: 2rem;
    }

    .search-form {
      display: flex;
      gap: 1rem;
      margin-bottom: 2rem;
    }

    .search-input {
      flex: 1;
      padding: 0.75rem;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 1rem;
    }

    .search-button {
      padding: 0.75rem 1.5rem;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 1rem;
    }

    .search-button:hover {
      background: #0056b3;
    }

    .search-button:disabled {
      background: #ccc;
      cursor: not-allowed;
    }

    .search-results {
      border-top: 1px solid #eee;
      padding-top: 1rem;
    }

    .result-item {
      padding: 1rem;
      border-bottom: 1px solid #f0f0f0;
    }

    .result-title {
      font-weight: 600;
      color: #333;
    }

    .result-author {
      color: #666;
      margin-top: 0.25rem;
    }
  `,
  render: ({ state, each, when }) => {
    return `
      <div class="book-search">
        <form class="search-form">
          <input 
            type="text" 
            name="search" 
            class="search-input" 
            placeholder="Search for books..."
            value="${state.searchTerm}"
          />
          <button 
            type="submit" 
            class="search-button"
            ${state.loading ? 'disabled' : ''}
          >
            ${state.loading ? 'Searching...' : 'Search'}
          </button>
        </form>

        ${when(state.searchResults.length > 0, `
          <div class="search-results">
            <h3>Search Results:</h3>
            ${each(state.searchResults, (book) => `
              <div class="result-item">
                <div class="result-title">${book.title}</div>
                <div class="result-author">by ${book.author}</div>
              </div>
            `)}
          </div>
        `)}
      </div>
    `;
  },
});

export default BookSearch;

Step 7: Handle Errors Gracefully

Let's add proper error handling to our getter:

api/books/getters.js

const getters = {
  books: {
    authorized: (input = {}, context = {}) => {
      return {
        authorized: !!context?.user,
        message: 'You must be logged in to view books.'
      };
    },
    input: {
      category: {
        type: 'string',
        required: false,
      },
      limit: {
        type: 'integer',
        required: false,
        max: 100,
      }
    },
    get: async (input = {}, context = {}) => {
      try {
        const query = {};
        
        if (input.category) {
          query.category = input.category;
        }

        if (context.user?.region) {
          query.availableRegions = { $in: [context.user.region] };
        }

        const limit = Math.min(input.limit || 20, 100);

        const books = await process.databases.mongodb
          .collection('books')
          .find(query)
          .limit(limit)
          .toArray();

        return books;
      } catch (error) {
        console.error('Error fetching books:', error);
        throw new Error('Failed to fetch books. Please try again later.');
      }
    }
  }
};

export default getters;

Step 8: Test Your Getter

You can test your getter by:

  1. Starting your Joystick app: joystick start
  2. Visiting the page that uses your BookList component
  3. Checking the browser's Network tab to see the API calls
  4. Testing different categories and limits

What You've Learned

Congratulations! You've learned how to:

  1. Create Getters: Define getter functions in the /api folder
  2. Register Getters: Add getters to your API schema
  3. Use Data Function: Fetch data during server-side rendering
  4. Input Validation: Validate and sanitize input parameters
  5. Authorization: Protect getters with authorization logic
  6. Client-Side Calls: Call getters from component event handlers
  7. Error Handling: Handle errors gracefully in both getters and components
  8. Data Refetching: Update component data dynamically

Best Practices

  • Keep getters focused: Each getter should have a single responsibility
  • Validate input: Always validate and sanitize input parameters
  • Use authorization: Protect sensitive data with proper authorization
  • Handle errors: Provide meaningful error messages
  • Limit data: Use pagination and limits to avoid performance issues
  • Cache when appropriate: Consider caching for frequently accessed data

Next Steps

Now you can:

  • Learn about using setters to change data
  • Explore more complex database queries
  • Implement search and filtering functionality
  • Add pagination to your data lists

Getters are the foundation of data retrieval in Joystick. Master them, and you'll be able to build powerful, data-driven applications!