Navigated to /docs/data.

Data

A hassle-free data layer for your Nokkio project.

Nokkio’s built-in data layer allows you to define your models and how they relate to one another, then begin using your data immediately without writing any backend code or setting up a database. Once your application is deployed, your data is securely stored and managed within Nokkio’s infrastructure.

There are six key touch points when implementing your application’s data with Nokkio:

  1. A schema, where you define your models and their properties.
  2. Page data, a page-level API where data can be loaded for a given route in your application using models generated from your schema.
  3. Suspense-compatible custom hooks, which are generated from your models and allow you to defer loading of less-important data.
  4. Form hooks and components for creating, updating, and deleting your data.
  5. Gates, for filtering input and limiting which requests can access and/or modify your data.
  6. Events, for taking additional action after records are created, updated, or deleted.

Schema

The shape of your data is defined in the schema.js file in your project's directory. Nokkio reads this schema and creates a database, backend endpoints, and client code necessary to access and manage your data.

The following schema.js example creates a Todo model with a few properties:

schema.js
/** @type {import('@nokkio/schema').Config} */
module.exports = ({ defineModel, types }) => {
  const Todo = defineModel('Todo', {
    description: types.string(),
    isComplete: types.bool(false),
  });

  return { Todo };
}

Nokkio’s schema definition supports relationships, unique constraints, default values, and more. See the Schema documentation for more information.

Page data

Once your schema is defined, you can use the generated models provided by @nokkio/magic to access data in your pages. Continuing with our example from before, let’s fetch all the Todos and show them on the application's home page:

pages/index.js
import { Todo } from "@nokkio/magic";

export async function getPageData() {
  return Todo.find({
    sort: "-createdAt",
  });
}

export default function Index() {
  const todos = usePageData();

  return (
    <ul>
      {todos.map((todo) => (
        <li>{todo.description}</li>
      ))}
    </ul>
  );
}

Behind the scenes, Nokkio manages all the communication with your database–there's no need to write any boilerplate backend code. For more on using getPageData, see the Page Module API.

Hooks

Nokkio's schema also produces hooks–also provided by @nokkio/magic–to access data in any component. These hooks are Suspense-compatible and can be used to defer the loading of less-important data until the initial page shell is has been sent to the client. The following example uses a hooks-based approach to accomplish the same result as the page data example from before:

pages/index.js
import { useTodos } from "@nokkio/magic";

export default function Index() {
  const todos = useTodos({
    sort: "-createdAt",
  });

  return (
    <ul>
      {todos.map((todo) => (
        <li>{todo.description}</li>
      ))}
    </ul>
  );
}

Whether you use page data or hooks depends on your use case. For the core data in your page, or data needed to construct the page's metadata, use page data. For secondary data that is not as critical to the initial render, use hooks.

Forms

The @nokkio/forms package can be used along with your application's models to quickly spin up HTML forms for creating, updating, or deleting your data. Building on the previous page data example, let’s add a form that creates a new Todo:

pages/index.js
import { Todo } from "@nokkio/magic";
import { useForm, Input } from "@nokkio/forms";

export async function getPageData() {
  return Todo.find({
    sort: "-createdAt",
  });
}

export default function Index() {
  const todos = usePageData();
  const { Form, isProcessing } = useForm(Todo);

  return (
    <div>
      <Form>
        <Input type="text" name="description" />
        <button disabled={isProcessing}>Create todo</button>
      </Form>

      <ul>
        {todos.map((todo) => (
          <li>{todo.description}</li>
        ))}
      </ul>
    </div>
  );
}

Try creating a Todo. Notice that the list automatically updates when the new Todo is added. Since Nokkio is privy to the entire data loop, it can track where data is used and granularly update your UI automatically, no need to track that state in your application.

Read more on using forms with Nokkio data.

Gates

By default, Nokkio allows any request to view or modify data. This reduces friction when prototyping, but before making your application broadly available you should put barriers in place to ensure data is only viewed or modified by the intended users. Nokkio provides a feature known as gates that allows you to guard your data by defining a function that is executed server-side before operations on your data are executed.

Gates are useful for two things: limiting access to data and modifying user input before it is persisted to the database. Continuing with our Todo example, we can do the following:

  1. Using beforeCreate, inspect the description and if it is prefixed with [done], isCompleted will be set to true.
  2. Using the beforeUpdate logic, Todos become immutable–any attempts to update a Todo will be denied and throw an error.
server/boot.js
import { Todo } from '@nokkio/magic';
import { BadRequestError } from '@nokkio/errors';

export default async function boot() {
  Todo.beforeCreate(async ({ fields }) => {
    if (fields.description.startsWith('[done]')) {
      return {
      	...fields,
        isCompleted: true,
      }
    }

    return fields;
  });

  Todo.beforeUpdate(async () => {
    throw new BadRequestError('Todos cannot be updated');
  });
}

Notice that hooks are setup via the server/boot.js file, as they run via Nokkio Endpoints. Gates are often paired with Nokkio's authentication system to limit access by the current authenticated user. Read more about gates.

Events Paid plans

Events are defined like Gates, but run after the action has completed. Use events to take action after your data has been created, updated, or deleted.

Like Gates, Events are also defined in server/boot.js. Events run outside of the request lifecycle, meaning they run sometime after the model has changed, usually within a few seconds.

In this example, a hypothetical sendEmail function can be used send an email any time a Todo is created:

server/boot.js
import { Todo } from '@nokkio/magic';
import { sendEmail } from 'server/email.js';

export default async function boot() {
  Todo.afterCreate(async ({ record }) => {
    sendEmail(
      'A new todo was created with description:' + record.description
    );
  });
}

Live subscriptions Paid plans

Whenever you use Nokkio's data in your components, it will automatically refresh when changes are made within the same user session. To update the UI when other users make changes, you may opt in to live subscriptions.

In this example, the list of todos will update whenever any user makes an update. The application opens a connection to nokk.io servers that streams updates from all other clients in near-realtime.

pages/index.js
import { Todo } from "@nokkio/magic";

export async function getPageData() {
  return Todo.find({
    live: true,
    sort: "-createdAt",
  });
}

export default function Index() {
  const todos = usePageData();

  return (
    <ul>
      {todos.map((todo) => (
        <li>{todo.description}</li>
      ))}
    </ul>
  );
}