Getting Started with the Prisma Framework (formerly Prisma 2) in React Native

But there was one major issue with Prisma. It had to be run through JVM and had memory issues. The Prisma Framework is rewritten in Rust, and it addresses the issues from Prisma 1.

Prisma 1 also required a server in front of your backend server, while with the Prisma Framework, the query engine is now a bundled executable that’s run alongside the backend on the same server.

The Prisma Framework consists of 2 standalone tools to tackle the problems of data access and migrations:

  • Photon: Type-safe and auto-generated database client (“ORM replacement”)
  • Lift: Declarative migration system with custom workflows

Photon is a type-safe database client that replaces traditional ORMs, and Lift allows us to create data models declaratively and perform database migrations.

The Prisma Framework also consists of Studio, which provides an Admin UI to support various database workflows.

So let’s get started with building a server with the Prisma Framework. We’ll be making a Programming Quotes App, so we’ll need to make a server that provides Programming Quotes.

You can find the complete code at the following GitHub repo:

Prerequisites

For this tutorial, you need a basic knowledge of React Native. You also need to understand React Hooks.

Since this tutorial is primarily focused on Prisma, it’s assumed that you already have a working knowledge of React and its basic concepts.

If you do not have a working knowledge of the above content, don’t worry. There are tons of tutorials available that will make you prepared to follow this post.

Throughout the course of this tutorial, we’ll be using yarn. If you don’t have yarn already installed, install it here.

To make sure we’re on the same page, these are the versions used in this tutorial:

  • Node v12.11.1
  • npm v6.11.3
  • npx v6.11.3
  • yarn v1.19.1
  • prisma2 v2.0.0-preview015
  • react-native-cli: 2.0.1
  • react-native: 0.61.2

Backend (Server Side)

Start a new Prisma 2 project by using the npx command as follows:

Alternatively, you can install the prisma2 CLI globally and then run the init command:

Run the interactive `prisma2 init` flow & select boilerplate

Select the following in the interactive prompts:

1. Select Starter Kit

2. Select JavaScript

3. Select GraphQL API

4. Select SQLite

Once terminated, the init command will have created an initial project setup in the server/ folder.

Now, open the schema.prisma file and replace it with the following:

generator photon {
  provider = "photonjs"
}

datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
}

model Quote {
  id      String @default(cuid()) @id
  author  String
  content String @unique
}

schema.prisma contains the data model as well as the configuration options.

Here, we specify that we want to connect to the SQLite Datasource called dev.db as well as target code generators like the photonjs generator.

Then, we define the data model Quote, which consists of id, author, and content.

  • id is a primary key of type String with a default value of cuid().
  • author is of type String.
  • content is of type String but with a constraint that it must be unique.

Now go ahead and create a quotes.json file in the prisma/ directory and paste the following into the quotes.json file:

[
  {
    "author": "Fred Brooks",
    "content": "What one programmer can do in one month, two programmers can do in two months."
  },
  {
    "author": "Addy Osmani",
    "content": "First do it, then do it right, then do it better."
  },
  {
    "author": "Edsger W. Dijkstra",
    "content": "Simplicity is prerequisite for reliability."
  },
  {
    "author": "Ken Thompson",
    "content": "One of my most productive days was throwing away 1,000 lines of code."
  }
]

The seed.js file should look like:

const {Photon} = require('@generated/photon');
const photon = new Photon();
const quotes = require('./quotes.json');

async function main() {
  for await (let quote of quotes) {
    const {content, author} = quote;
    await photon.quotes.create({
      data: {
        author,
        content,
      },
    });
  }
}

main()
  .catch(e => console.error(e))
  .finally(async () => {
    await photon.disconnect();
  });

This file takes the dummy data from the quotes.json file and adds it to the SQLite database.

Now go inside the src/index.js file and remove its contents. We’ll start adding content from scratch.

First, go ahead and import the necessary packages and declare some constants:

const {GraphQLServer} = require('graphql-yoga');
const {join} = require('path');
const {
  makeSchema,
  objectType,
  queryType,
  mutationType,
  stringArg,
} = require('nexus');
const {Photon} = require('@generated/photon');
const {nexusPrismaPlugin} = require('nexus-prisma');

const photon = new Photon();

We have declared a constant photon which instantiates a new Photon class.

Let’s now declare our Quote model. Paste the code below:

const Quote = objectType({
  name: 'Quote',
  definition(t) {
    t.model.id();
    t.model.content();
    t.model.author();
  },
});

The name parameter should be the same as defined in the schema.prisma file.

The definition function lets you expose a particular set of fields wherever Quote is referenced. Here, we expose id, content, and author fields.

Below that, paste the Query constant:

const Query = queryType({
  definition(t) {
    t.crud.quotes();
    t.list.field('filterQuotesByAuthor', {
      type: 'Quote',
      args: {
        author: stringArg(),
      },
      resolve: (_, {author}, ctx) => {
        return ctx.photon.quotes.findMany({
          where: {
            author: {contains: author},
          },
        });
      },
    });
  },
});

The Photon generator generates an API that exposes CRUD functions on the Quote model. This is what allows us to expose the t.crud.quotes() method.

Then we make a field named filterQuotesByAuthor. The return type is Quote. It takes an argument author, which is of type String. This argument is received in the resolve function as the 2nd argument. The 3rd argument is the context ctx.

We can call context to get the contents from our SQLite database.

We use the findMany method on quotes, which returns a list of objects. We find all the quotes with a partially matched author.

The contains keyword denotes it’s partially matched. For a complete match, you can replace author: {contains: author} with author: author. The complete match will require the complete name of the author to get the results.

Below Query, paste Mutation as follows:

const Mutation = mutationType({
  definition(t) {
    t.crud.createOneQuote({alias: 'createQuote'});
    t.crud.deleteOneQuote({alias: 'deleteQuote'});
  },
});

The CRUD API here exposes createOneQuote and deleteOneQuote.

createOneQuote, as the name suggests, creates a quote, whereas deleteOneQuote deletes a quote.createOneQuote is aliased as createQuote, so while calling the mutation, we call createQuote rather than calling createOneQuote.

Similarly, we call deleteQuote instead of deleteOneQuote.

Finally, paste the following code below Mutation:

const schema = makeSchema({
  types: [Query, Mutation, Quote],
  plugins: [nexusPrismaPlugin()],
  outputs: {
    schema: join(__dirname, '/schema.graphql'),
  },
  typegenAutoConfig: {
    sources: [
      {
        source: '@generated/photon',
        alias: 'photon',
      },
    ],
  },
});

We use the makeSchema method from the nexus package to combine our model Quote and add Query and Mutation to the types array. We also add nexusPrismaPlugin to our plugins array. Then we output our schema in the same directory and name it schema.graphql. Finally, we auto-generate types in the node_modules/@generated/photon folder.

At last, paste the following:

const server = new GraphQLServer({
  schema,
  context: request => {
    return {
      ...request,
      photon,
    };
  },
});

server.start(() =>
  console.log(
    '🚀 Server ready at: http://localhost:4000n⭐️ See sample queries: http://pris.ly/e/js/graphql#6-using-the-graphql-api',
  ),
);

module.exports = {Quote};

Finally, we start our server at http://localhost:4000/. Port 4000 is the default port for graphql-yoga. You can change the port as suggested here.

Let’s start the server now. But first, we need to make sure our latest schema changes are written to the node_modules/@generated/photon directory. This happens when you run prisma2 generate.

Migrating your database with Lift follows a 2-step process:

  1. Save a new migration (migrations are represented as directories on the file system)
  2. Run the migration (to actually migrate the schema of the underlying database)

In CLI commands, these steps can be performed as follows (the CLI steps are in the process of being updated to match):

Again you’d have to replace prisma2 with ./node_modules/.bin/prisma2 if you haven’t installed it globally.

Now the migration process is done. We’ve successfully created the table.

Generate the database schema by typing the following in the terminal:

Now we can seed our database with initial values.

Go ahead and run the following command in the terminal:

This will seed our database with 4 quotes as specified in our ./quotes.json file.

Now you can run the server by typing:

This will run your server at http://localhost:4000/, with which you can open and query all the APIs you’ve made.

Query all quotes

query quotes {
  quotes {
    id
    content
    author
  }
}

Filter Quotes by Author

query filterQuotesByAuthor {
  filterQuotesByAuthor(author: "Addy") {
    id
    content
    author
  }
}

Create Quote Mutation

mutation createQuote {
  createQuote(
    data: {
      author: "Addy Osmani"
      content: "Improving performance is a journey. Small changes can often lead to big gains."
    }
  ) {
    id
    author
    content
  }
}

Delete Quote Mutation

mutation deleteQuote {
  deleteQuote(where: { id: "ck2ndht9k0000bnv5mduydes9" }) {
    id
  }
}

Docs panel in GraphiQL Editor

You can click on the Docs panel in the GraphiQL editor to look closer at the queries and the mutation as shown below:

This helps us write the queries and the mutations, which in turn makes writing client-side code easy.

Now that our backend works perfectly, let’s make the frontend.

Frontend (Client-Side)

Start a new React Native project by using the npx command as follows:

Alternatively, you can install the react-native-cli globally and run the init command then:

This bootstraps a new React Native project using react-native-cli. To run the project, type this:

This will run the bundler. We then need to specify on which device we need to run this project. To run it on iOS, type:

To run it on Android, type:

It should look like this:

Now replace App.js with the following:

import {ApolloProvider} from '@apollo/react-hooks';
import ApolloClient from 'apollo-boost';
import React from 'react';
import {SafeAreaView, StatusBar, StyleSheet, Text} from 'react-native';

const client = new ApolloClient({
  uri: 'http://localhost:4000/',
});

const App = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <ApolloProvider client={client}>
        <SafeAreaView>
          <Text style={styles.text}>Programming Quotes</Text>
        </SafeAreaView>
      </ApolloProvider>
    </>
  );
};

const styles = StyleSheet.create({
  text: {
    fontSize: 16,
    margin: 10,
  },
});

export default App;

Go ahead and create a components folder.

Inside this components folder, create a graphql folder.

Inside the graphql folder, create mutations and queries folders.

Inside the queries folder, create a file named quotes.js.

Inside quotes.js, paste the following:

import {gql} from 'apollo-boost';

export const GET_QUOTES_QUERY = gql`
  query quotes {
    quotes {
      id
      content
      author
    }
  }
`;

Notice that the above query is similar to what we typed in the GraphiQL editor. This is how GraphQL is used. First, we type the query in the GraphiQL editor and see if it gives the data that we need, and then we just copy-paste it into the application.

Inside the mutations folder, create 2 files: createQuote.js and deleteQuote.js.

Inside createQuote.js, paste the following:

import {gql} from 'apollo-boost';

export const CREATE_QUOTE_MUTATION = gql`
  mutation createQuote($author: String!, $content: String!) {
    createQuote(data: {author: $author, content: $content}) {
      id
      content
      author
    }
  }
`;

Again, we’ve copied the mutation from our GraphiQL editor above. The main difference is we’ve replaced the hardcoded value with a variable so we can type in whatever a user has specified.

Inside deleteQuote.js, paste the following:

import {gql} from 'apollo-boost';

export const DELETE_QUOTE_MUTATION = gql`
  mutation deleteQuote($id: ID) {
    deleteQuote(where: {id: $id}) {
      id
    }
  }
`;

Now create 2 files in the components/ folder; namely Error.js and Loading.js .

In Error.js, paste the following:

import React from 'react';
import {StyleSheet, Text, View} from 'react-native';

export const Error = () => (
  <View>
    <Text style={styles.error}>Error...</Text>
  </View>
);

const styles = StyleSheet.create({
  error: {
    color: 'red',
  },
});

In Loading.js, paste the following:

import React from 'react';
import {ActivityIndicator} from 'react-native';

export const Loading = () => <ActivityIndicator size="small" />;

These components will be used later in the application. Let’s now display all the quotes in the application. Firstly, open the App.js file and paste the following code below the Text:

<CreateQuote />
<ListQuotes />

Also, make sure to import those components:

import {CreateQuote} from './components/CreateQuote';
import {ListQuotes} from './components/ListQuotes';

The whole App.js file should now look like:

import {ApolloProvider} from '@apollo/react-hooks';
import ApolloClient from 'apollo-boost';
import React from 'react';
import {SafeAreaView, StatusBar, StyleSheet, Text} from 'react-native';
import {CreateQuote} from './components/CreateQuote';
import {ListQuotes} from './components/ListQuotes';

const client = new ApolloClient({
  uri: 'http://localhost:4000/',
});

const App = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <ApolloProvider client={client}>
        <SafeAreaView>
          <Text style={styles.text}>Programming Quotes</Text>
          <CreateQuote />
          <ListQuotes />
        </SafeAreaView>
      </ApolloProvider>
    </>
  );
};

const styles = StyleSheet.create({
  text: {
    fontSize: 16,
    margin: 10,
  },
});

export default App;

Make sure to comment out CreateQuote above for now so that we don’t get an error. Create a file in the components/ directory named ListQuotes.js.

Now go ahead and paste the following in it:

import {useMutation, useQuery} from '@apollo/react-hooks';
import React from 'react';
import {Button, FlatList, StyleSheet, Text, View} from 'react-native';
import {Error} from './Error';
import {DELETE_QUOTE_MUTATION} from './graphql/mutations/deleteQuote';
import {GET_QUOTES_QUERY} from './graphql/queries/quotes';
import {Loading} from './Loading';

export const ListQuotes = () => {
  const {loading, error, data} = useQuery(GET_QUOTES_QUERY);

  if (loading) {
    return <Loading />;
  }
  if (error) {
    return <Error />;
  }
  const quotes = data.quotes;
  return (
    <View style={styles.container}>
      {!quotes.length ? (
        <Text>No quotes in the database. Add one :)</Text>
      ) : (
        <FlatList
          data={quotes}
          renderItem={({item}) => {
            return (
              <View style={styles.quoteWrapper}>
                <Text style={styles.content}>“{item.content}”</Text>
                <Text style={styles.author}> – {item.author}</Text>
              </View>
            );
          }}
          keyExtractor={item => item.id}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    margin: 10,
  },
  quoteWrapper: {
    margin: 8,
  },
  content: {
    fontSize: 16,
    color: '#3E4C59',
  },
  author: {
    marginTop: 2,
    marginBottom: 2,
    fontSize: 14,
    color: '#616E7C',
  },
});

Here, we use the useQuery API from @apollo/react-hooks. We pass in GET_QUOTES_QUERY to it. And we get back 3 parameters, loading, error, and data.

We show the <Loading /> component if loading is true.

We show the <Error /> component if error is true.

Then, if we don’t have quotes, we display a friendly message No quotes in the database. Add one :).

If we do have quotes in the database, then we display the quotes.

We use FlatList to render the quotes.

Next, let’s implement delete functionality. Go ahead and add the following code just below the useQuery function and above the loading if-statement:

const [deleteQuote] = useMutation(DELETE_QUOTE_MUTATION);

We’ll use the deleteQuote function below. Add the following code just below the closing View tag inside the FlatList component:

<Button
  color="#ff0000"
  title="Delete"
  onPress={() => removeQuote(item.id, deleteQuote)}
/>

The above code just creates a Button, which calls the removeQuote function, which we haven’t created yet.

Now add the removeQuote function just below the imports:

const removeQuote = (id, deleteQuote) => {
  deleteQuote({
    variables: {
      id,
    },
    update: (cache, {data}) => {
      const {quotes} = cache.readQuery({
        query: GET_QUOTES_QUERY,
      });
      cache.writeQuery({
        query: GET_QUOTES_QUERY,
        data: {
          quotes: quotes.filter(quote => quote.id !== id),
        },
      });
    },
  });
};

This function calls the deleteQuote mutation and optimistically updates the UI to remove deleted results without having to refresh the app.

The whole ListQuotes.js file should now look like:

import {useMutation, useQuery} from '@apollo/react-hooks';
import React from 'react';
import {Button, FlatList, StyleSheet, Text, View} from 'react-native';
import {Error} from './Error';
import {DELETE_QUOTE_MUTATION} from './graphql/mutations/deleteQuote';
import {GET_QUOTES_QUERY} from './graphql/queries/quotes';
import {Loading} from './Loading';

const removeQuote = (id, deleteQuote) => {
  deleteQuote({
    variables: {
      id,
    },
    update: (cache, {data}) => {
      const {quotes} = cache.readQuery({
        query: GET_QUOTES_QUERY,
      });
      cache.writeQuery({
        query: GET_QUOTES_QUERY,
        data: {
          quotes: quotes.filter(quote => quote.id !== id),
        },
      });
    },
  });
};

export const ListQuotes = () => {
  const {loading, error, data} = useQuery(GET_QUOTES_QUERY);
  const [deleteQuote] = useMutation(DELETE_QUOTE_MUTATION);

  if (loading) {
    return <Loading />;
  }
  if (error) {
    return <Error />;
  }
  const quotes = data.quotes;
  return (
    <View style={styles.container}>
      {!quotes.length ? (
        <Text>No quotes in the database. Add one :)</Text>
      ) : (
        <FlatList
          data={quotes}
          renderItem={({item}) => {
            return (
              <View style={styles.quoteWrapper}>
                <Text style={styles.content}>“{item.content}”</Text>
                <Text style={styles.author}> – {item.author}</Text>
                <Button
                  color="#ff0000"
                  title="Delete"
                  onPress={() => removeQuote(item.id, deleteQuote)}
                />
              </View>
            );
          }}
          keyExtractor={item => item.id}
        />
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    margin: 10,
  },
  quoteWrapper: {
    margin: 8,
  },
  content: {
    fontSize: 16,
    color: '#3E4C59',
  },
  author: {
    marginTop: 2,
    marginBottom: 2,
    fontSize: 14,
    color: '#616E7C',
  },
});

Make you haven’t missed anything. Otherwise, it won’t delete the quote properly. Your app should now look like:

You can try deleting a quote now, and it will be deleted. Uncomment CreateQuote in App.js file now. Go ahead and create a new file called CreateQuote.js.

Paste the following in it:

import {useMutation} from '@apollo/react-hooks';
import React, {useState} from 'react';
import {Button, StyleSheet, TextInput, View} from 'react-native';
import {Error} from './Error';
import {CREATE_QUOTE_MUTATION} from './graphql/mutations/createQuote';
import {GET_QUOTES_QUERY} from './graphql/queries/quotes';

const saveQuote = (quote, author, changeQuote, changeAuthor, createQuote) => {
  if (quote.trim() === '' || author.trim() === '') {
    return;
  }
  createQuote({
    variables: {
      content: quote,
      author,
    },
    update: (cache, {data}) => {
      const {quotes} = cache.readQuery({
        query: GET_QUOTES_QUERY,
      });

      cache.writeQuery({
        query: GET_QUOTES_QUERY,
        data: {
          quotes: quotes.concat(data.createQuote),
        },
      });
    },
  });
  changeQuote('');
  changeAuthor('');
};

export const CreateQuote = () => {
  const [quote, changeQuote] = useState('');
  const [author, changeAuthor] = useState('');
  const [createQuote, {error, data}] = useMutation(CREATE_QUOTE_MUTATION);
  if (error) {
    return <Error />;
  }
  return (
    <View style={styles.wrapper}>
      <TextInput
        style={styles.textInput}
        placeholder="Enter the quote"
        onChangeText={quoteValue => changeQuote(quoteValue)}
        value={quote}
      />
      <TextInput
        style={styles.textInput}
        placeholder="Enter the author"
        onChangeText={authorValue => changeAuthor(authorValue)}
        value={author}
      />
      <Button
        title="Save Quote"
        color="#3E4C59"
        onPress={() =>
          saveQuote(quote, author, changeQuote, changeAuthor, createQuote)
        }
      />
    </View>
  );
};

const styles = StyleSheet.create({
  wrapper: {
    margin: 8,
  },
  textInput: {
    borderWidth: 1,
    marginTop: 4,
    marginBottom: 4,
    padding: 8,
    fontSize: 16,
    color: '#3E4C59',
  },
});

Here, we create 2 inputs, one for quote and one for author. We then use React Hooks to get the data in our 2 variables, quote and author. Finally, when the user taps the Button, we call the saveQuote function.

The saveQuote function calls the mutation createQuote with the variables content and author. It also uses the update function to append the newly created quote to the UI so you can see the quote without refreshing the app. And finally, it makes the input fields blank after it’s done creating the quote.

Your app should now look like:

Conclusion

We have built a complete functional application from scratch with React Native and the Prisma Framework (formerly, Prisma 2).

The Prisma Framework allows us to write a query in our language of choice, and then it maps everything out to a database so we don’t have to worry about writing it in the database language. We can easily swap out any database by using it. Currently, it’s in beta, but you can still use it to create applications at lightning speed.

You can now create any fully-functional application using React Native and Prisma 2. If you’d like to explore the full code for this project, you can check out this GitHub repo:

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square