Written by Austin Roy Omondi✏️
React Native is a great option when it comes to developing mobile applications for both iOS and Android smartphones. With React Native you can write an application that works on both platforms with the only difference coming at the view level, with iOS and Android rendering differently. In this two-part series, we’ll be looking at how we can develop an application with React Native and GraphQL. To do this, we’ll be building a simple note-taking app that allows our user to add notes, view, edit and delete them.
For the first part, we’ll be building a GraphQL server with a PostgreSQL database that will serve as the backend of our application. In the second part (blog post to come later) we will build an application with React Native that will consume data from our server.
Let’s get started with our server setup. The folder structure for our project will be as shown in this repo.
In order to build our application, you will need to be familiar with:
- NodeJS
- Express
- Databases
- Mobile development
How to set up an Apollo server with Express
Our backend server will run on Node JS and Express. We’ll have a few initial dependencies to install, but first, we’ll need to initiate our app, run:
npm init
Now let us install our first set of dependencies:
npm install apollo-server apollo-server-express express graphql cors --save
One of the dependencies we have installed is apollo-server
, Apollo Server is the best way to quickly build a production-ready, self-documenting API for GraphQL clients, using data from any source. While we are using it with Express, it may be used with several popular libraries such as Hapi and Koa.
Now we are all set to start creating our server. Add the following code to src/index.js
.
import cors from 'cors';
import express from 'express';
import {
ApolloServer
} from 'apollo-server-express';
const app = express();
app.use(cors());
const server = new ApolloServer({});
server.applyMiddleware({
app,
path: '/graphql'
});
app.listen({
port: 8000
}, () => {
console.log('Apollo Server on http://localhost:8000/graphql');
});
Let’s go through what we have so far, in the code above, we have initialized an Express application as well as an Apollo server instance. Using Apollo Server’s applyMiddleware()
method, you can opt-in any middleware, which in our case is the Express app. Also, you can specify the path for your GraphQL API endpoint.
You may also note we applied cors
to our Express application, this is to allow external domains to access our application.
Our next step would be to define our schema and resolvers for the Apollo Server instance. But first, let’s cover schema and resolvers.
Schema
The GraphQL schema provided to the Apollo Server is all of the available data for reading and writing data via GraphQL. The schema contains type definitions from a mandatory top-level Query
type that allows reading of data followed by field and nested fields. These are defined using the GraphQL Schema Language and allows us to talk about GraphQL schemas in a language-agnostic way.
Here’s what’s in our schema/notes.js
:
import {
gql
} from 'apollo-server-express';
export default gql `
extend type Query {
notes: [Note!]
note(id: ID!): Note!
}
type Note {
id: ID!
text: String!
}
`;
Let’s take a look at our schema definition for some notable terms we need to be aware of. We have already covered the Query
type, but let’s look at the rest:
-
notes
is defined as[Note!]
which represents an array ofNote
objects. The exclamation mark means it is non-nullable, this means you can always expect an array (with zero or more items) when you query thenotes
field - Each
Note
object contains anid
field defined asID
. TheID
scalar type represents a unique identifier, often used to re-fetch an object or as the key for a cache. It is serialized the same way a String is serialized. However, defining it as anID
signifies that it is not intended to be human‐readable - The
Note
object also has atext
field defined asString
which is one of the built-in scalar types that is a sequence of UTF-8 characters - You will also notice the
note
field is provided withid
options in the form of(id: ID!)
. This allows for the use of GraphQL arguments to refine queries. This allows us to fetch a single note by providing it’sID
in our query. The argument is also non-nullable as it must be provided when fetching a single note.
The schema above shall be placed in src/schema/notes.js
. We can then have a index.js
file inside the src/schema
directory with the following code:
import { gql } from "apollo-server-express";
import noteSchema from "./notes";
const linkSchema = gql`
type Query {
_: Boolean
}
type Mutation {
_: Boolean
}
type Subscription {
_: Boolean
}
`;
export default [linkSchema, noteSchema];
Resolvers
Resolvers are responsible for manipulating and returning data, think of them as the query handlers. Each top-level query in the Query
type has a resolver but we’ll make our own per field. The resolvers are grouped in a JavaScript object, often called a resolver map.
Let’s create a notes.js
file inside src/resolvers
and add the following code to it:
export default {
Query: {
notes: (parent, args, {
models
}) => {
return Object.values(models.notes)
},
note: (parent, {
id
}, {
models
}) => {
return models.notes[id]
}
}
}
The above resolvers allow us to query either individual notes using the note
field and provide an id
argument or an array of notes using the notes
field. We’ll clean up our resolvers once we link our app to the database using sequelize and add Mutations which will allow us to carry out operations other than reading on our data. For now, these resolve dummy data that we’ll have in src/models/index.js
which contains the following code:
let notes = {
1: {
id: '1',
text: 'Hello World',
},
2: {
id: '2',
text: 'By World',
},
};
export default {
notes
};
Now that we have the framework set up for how our data will be stored let’s look at how data manipulation will be done using queries and mutations.
Queries and mutations
Queries and mutations in GraphQL allow us to access and manipulate data on a GraphQL server. Queries are in charge of read operations whereas mutations are in charge of create, update and delete operations.
To create our queries and mutations, we must first define them in our schema. Let’s edit src/schema/notes.js
as follows:
import {
gql
} from 'apollo-server-express';
export default gql `
extend type Query {
notes: [Note!]
note(id: ID!): Note!
}
extend type Mutation {
createNewNote(text: String!): Note!
deleteNote(id: ID!): Boolean!
updateNote(id: ID!, text: String!): Note!
}
type Note {
id: ID!
text: String!
}
`;
Queries have already been defined earlier so our new addition is the Mutation
type. From the types defined, you can get an idea of what the returned data will look like. For instance, createNewNote
takes a text
argument that is a string and generates and returns a Note
object, updateNote
is similar but it also takes the id
so that it can find the note to be updated. Meanwhile, deleteNote
takes an id
boolean that is used to identify the target note and returns a boolean indicating whether the note has been deleted successfully or not.
This is how you can test your queries. Type the following code in your graphql playground:
Get all notes
Fetches and returns all notes saved.
query {
notes {
id
text
}
}
Next hit the Execute Query button and you should be able to see the following result:
Result
{
"data": {
"notes": [
{
"id": "1",
"text": "Hello World"
},
{
"id": "3",
"text": "By World"
}
]
}
}
Get note by id
This returns a piece of the data matching the given argument id
query{
note(id: "1") {
id
text
}
}
Result
{
"data": {
"note": {
"id": "1",
"text": "Hello World"
}
}
Next, we’ll need to write a Mutation resolver. Let’s editsrc/resolvers/notes.js
like this:
import uuidv4 from 'uuid/v4';
export default {
Query: {
notes: (parent, args, {
models
}) => {
return Object.values(models.notes)
},
note: (parent, {
id
}, {
models
}) => {
return models.notes[id]
}
},
Mutation: {
createNewNote: (parent, {
text
}, {
models
}) => {
const id = uuidv4();
const newNote = {
id,
text
}
models.notes[id] = newNote;
return newNote;
},
deleteNote: (parent, {
id
}, {
models
}) => {
const {
[id]: note, ...otherNotes
} = models.notes
if (!note) {
return false
}
models.notes = otherNotes
return true
},
}
For the createNewNote
Mutation we are generating the unique id
using the uuid/v4
library so we’ll need to install it by running:
npm install uuid/v4 --save
The resolver takes a text argument, generates a unique id
and uses them to create a Note
object which is then appended to the list of notes. It then returns the new note that has been created.
The deleteNote
Mutation takes an id
argument, finds a note and creates a new notes
object without the note to be deleted and replaces the old notes
object with the new one. It returns false
if the note is not found and true
when the note is found and deleted.
The updateNote
Mutation takes the id
and text
as arguments and updates the Note
in the notes
object accordingly, returning the updated note at the end.
We can now access these endpoints through the GraphQL playground when we run our application by running npm start
and going to localhost:3000/graphql
on the browser. You can then try out these actions:
Create a new note
Takes a text argument and creates and returns a note.
mutation {
createNewNote( text: "Hello GraphQl") {
id
text
}
}
Result
{
"data": {
"createNewNote": {
"id": "3",
"text": "Hello GraphQl"
}
}
}
Update note
Takes text and id arguments and updates and returns a note.
mutation {
updateNote(id:3, text:"Graphql is awesome") {
id
text
}
}
Result
{
"data": {
"updateNote": {
"id": "4",
"text": "Graphql is awesome"
}
}
}
Delete note
Takes an id argument and deletes the matching note.
mutation {
deleteNote(id:5)
}
Adding PostgreSQL with Sequelize
We have most of our app ready to go so the next thing we need to look at is how we can save our data to a database. Our database will be PostgreSQL so go ahead and install it from their site then start it.
After that, create a database called mynotes
. To do this you can run the command psql
in your terminal to open up the PosgreSQL CLI. Once open run:
CREATE DATABASE mynotes;
CREATE USER postgres;
GRANT ALL PRIVILEGES ON DATABASE members TO postgres;
Next you’ll want to add PostgreSQL for Node.js and Sequelize (ORM) to your project. Run:
npm install pg sequelize --save
The next step is to update our models to prepare to link to the database as shown below. The model defines the shape of each piece of data as it will be stored in the database:
const note = (sequelize, DataTypes) => {
const Note = sequelize.define("note", {
text: DataTypes.STRING
});
return Note;
};
export default note;
Next, connect to your database from within your application in the src/models/index.js
file:
import Sequelize from "sequelize";
const sequelize = new Sequelize(
process.env.DATABASE,
process.env.DATABASE_USER,
process.env.DATABASE_PASSWORD,
{
dialect: "postgres"
}
);
const models = {
Note: sequelize.import("./notes")
};
Object.keys(models).forEach((key) => {
if ("associate" in models[key]) {
models[key].associate(models);
}
});
export { sequelize };
export default models;
You’ll need to define the database name, a database super-user, and the user’s password. You may also want to define a database dialect because Sequelize supports other databases as well.
In the same file, you can physically associate all your models with each other if you have multiple models (hence the foreEach
loop above) to expose them to your application as data access layer (models) for the database. In our case, we only have one model but this is still good to know.
Now let’s create a .env
file at the root directory of the project to hold some critical data as environment variables. The database credentials (database name, database super user name, database super-user password) can be stored as environment variables along with the port on which we wish to run our server.
In the .env
file add the following variables or change them to your liking:
PORT=3000
DATABASE=mynotes
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
Next, let’s update src/index.js
to get our databases synchronized when our application is started. Make the following changes:
...
import 'dotenv/config';
import models, {
sequelize
} from './models';
...
sequelize.sync().then(async () => {
app.listen({
port: 8000 // you could change this to process.env.PORT if you wish
}, () => {
console.log('Apollo Server on http://localhost:8000/graphql');
});
});
We’ve now linked our application to the database so what we need to do next is replace the logic in our resolvers.
How to connect resolvers to the database
We had dummy data before but with Sequelize we can now allow us to sync our data with our database, allowing for data persistence. To make this happen, we need to update our resolver in src/resolvers/notes.js
to integrate the Sequelize API.
export default {
Query: {
notes: async (parent, args, { models }) => {
return await models.Note.findAll();
},
note: async (parent, { id }, { models }) => {
return await models.Note.findByPk(id);
}
},
Mutation: {
createNewNote: async (parent, { text }, { models }) => {
return await models.Note.create({
text
});
},
deleteNote: async (parent, { id }, { models }) => {
return await models.Note.destroy({
where: {
id
}
});
},
updateNote: async (parent, { id, text }, { models }) => {
await models.Note.update(
{
text
},
{
where: {
id: id
}
}
);
const updatedNote = await models.Note.findByPk(id, {
include
});
return updatedNote;
}
}
};
You will notice we no longer need uuid/v4
as Sequelize automatically handles the assignment of ids in our database. The findAll()
and findByPk()
are commonly used Sequelize methods for database operations, findAll()
returns all entries in a certain table whereas findPk()
identifies and returns entries that fit a certain criterion such as a matching id
in our case.
Other Sequelize methods we make use of include update()
and destroy()
to update and delete records respectively.
Along with these, we use async/await to implement promise-based asynchronous Javascript requests to our database.
Conclusion
Great! We are all ready to go, the next step is to integrate our GraphQL backend with our front-end app, which we’ll build for mobile in part two of this tutorial. We’ll be working with Apollo and React Native which should be exciting. Stay tuned!
Editor's note: Seeing something wrong with this post? You can find the correct version here.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post How to create a React Native app with PostgreSQL and GraphQL: Part 1 appeared first on LogRocket Blog.
Top comments (0)