Building an App from scratch

There are two parts to most web apps: the backend and the frontend. The backend is usually an API of some kind and the frontend is mobile and/or web apps built in React, Vue, Android, iOS, etc.

Super Graph will instantly give you a powerful and high-performance GraphQL API that can be your apps' backend. Let's get started, I promise you it's super easy and will save you weeks to months of your life.

For this example we will create a web e-commerce store. This example app can be found in repo. https://github.com/dosco/super-graph/tree/master/examples/webshop

Try the example app

Below we explain how this example app was built and other details around useing Super Graph to make you more productive.

git clone https://github.com/dosco/super-graph.git
cd super-graph/examples/webshop
docker-compose run api db:setup
docker-compose up

Install Super Graph

go get github.com/dosco/super-graph

Create the app

Let's call our app Webshop.

super-graph new webshop
cd webshop

Add a database schema

super-graph db:new users
super-graph db:new products
super-graph db:new sections
super-graph db:new customers
super-graph db:new purchases
super-graph db:new notifications
# delete the example migration
rm -rf config/migrations/0_init.sql

Defines tables and database relationships

Be sure to define primary keys to all of your tables and to use foreign keys to define relationships between tables. In the example below the products table has a foreign key relationhsip user_id to the users table. It looks like this user_id bigint REFERENCES users(id)

vim config/migrations/1_users.sql
-- Write your migrate up statements here
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
full_name character varying NOT NULL,
phone character varying,
avatar character varying,
email character varying NOT NULL DEFAULT ''::character varying,
encrypted_password character varying NOT NULL DEFAULT ''::character varying,
reset_password_token character varying,
reset_password_sent_at timestamp without time zone,
remember_created_at timestamp without time zone,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
-- Indices -------------------------------------------------------
CREATE UNIQUE INDEX index_users_on_email ON users(email text_ops);
CREATE UNIQUE INDEX index_users_on_reset_password_token ON users(reset_password_token text_ops);
---- create above / drop below ----
DROP TABLE users;
-- Write your migrate down statements here. If this migration is irreversible
-- Then delete the separator line above.
vim config/migrations/2_products.sql
-- Write your migrate up statements here
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name text,
description text,
price numeric(7,2),
tags text[],
category_ids bigint[] NOT NULL,
user_id bigint REFERENCES users(id),
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
-- tsvector column needed for full-text search
tsv tsvector GENERATED ALWAYS
AS (to_tsvector('english', description)) STORED,
);
-- Indices -------------------------------------------------------
CREATE INDEX index_products_on_tsv ON products USING GIN (tsv tsvector_ops);
CREATE INDEX index_products_on_user_id ON products(user_id int8_ops);
---- create above / drop below ----
DROP TABLE products;
-- Write your migrate down statements here. If this migration is irreversible
-- Then delete the separator line above

Create a seed script

This step is optional. A seed script inserts fake data into the database. It helps frontend developers if they already have some fake data to work with. The seed script is written in javascript and data is inserted into the database using GraphQL.

If you don't plan to use it then delete the sample seed file rm -rf config/seed.js

// Example script to seed database
var pwd = "12345";
// lets create 100 fake users
for (i = 0; i < 100; i++) {
// these fake data functions are built into super graph
var data = {
full_name: fake.name(),
avatar: fake.avatar_url(),
phone: fake.phone(),
email: "user" + i + "@demo.com",
password: pwd,
password_confirmation: pwd,
created_at: "now",
updated_at: "now",
};
// graphql mutation query to insert user
var res = graphql(
" \
mutation { \
user(insert: $data) { \
id \
} \
}",
// graphql variables like $data
{ data: data },
// current authenicated user id
{ user_id: -1 }
);
}

Setup the database

This will create the database, run the migrations and the seed file. Once it's set up to reset use the db:reset command-line argument.

docker-compose run api db:setup

Start the Webshop API

docker-compose up

Access the Super Graph UI

The Super Graph web UI is used to build and test queries. It supports auto-completion which makes it easy to craft queries. Open your web browser and visit the below url.

http://localhost:8080

Fetch data with GraphQL

query {
products {
id
name
description
customers {
id
email
}
}
}
{
"data": {
"products": [
{
"id": 1,
"name": "Oak Aged Yeti Imperial Stout",
"customers": [
{
"id": 2,
"email": "johannahagenes@considine.com"
},
{
"id": 2,
"email": "johannahagenes@considine.com"
}
],
"description": "Belgian And French Ale, Galena, Carapils"
},
...
]
}
}

Add validations

Validations can be added in the database schema -- this way nothing can bypass them and put invalid data into your database.

CREATE TABLE products (
name text CHECK (length(name) > 1 AND length(name) < 50),
...
)

Full text search

Postgres has great full-text search built-in. There's no reason to complicate your setup by adding another service just for search. Enabling full-text search on a table requires you to add a single column (term-vector column) to it.

CREATE TABLE products (
-- tsv column is used by full-text search
tsv tsvector GENERATED ALWAYS
AS (to_tsvector('english', name) || to_tsvector('english',description)) STORED,
...

Autocomplete

Implementing autocomplete does not need a tsv column. Instead, columns that you'd like to be used with autocomplete need an extra index of the type text_pattern_ops.

CREATE INDEX product_name_autocomplete_index ON products(name text_pattern_ops);

Now use the below query and set the $prefix variable to the entered text value you want to autocomplete on.

query {
products(
where: { name: { ilike: $prefix } }
order_by: { updated_at: desc }
) {
...Product
}
}

Using GraphQL fragments

Some of you unfamiliar with GraphQL might wonder what ...Product is in the code above. It's called a fragment. Fragments save you from having to retype the same set of columns all over. Most GraphQL clients like apollo-client or urql have a way to manage fragments and share then across your queries.

fragment Product on products {
id
name
description
}
query {
products(limit: 10) {
...Product
}
}

Fetch the discuessions about a product

query {
product(id: $id) {
name
comments {
id
body
replies: comments(find: "children", limit: 5, order_by: { created_at: desc }) {
id
body
}
}
}
}

Create new product

The following GraphQL query and variables together will insert a new product into the database. The connect keyword will find a user whose id equals 5 and set the user_id field on the product to that user's id.

{
"data": {
"name": "Nice Handbag",
"description": "This is a really nice handbag",
"price": 200,
"user": {
"connect": { "id": 5 }
}
}
}
mutation {
product(insert: $data) {
id
name
}
}

Update a product

This will update a product with id = 12 and change its name, description and price.

{
"data": {
"name": "Not nice Handbag",
"description": "This is not really that nice a bag",
"price": 100
}
}
mutation {
product(update: $data, where: { id: { eq: 12 } }) {
id
name
}
}

Create a user, product with tags and categories

There is so much going on here. We are creating a user, his product and assigning some tags and categories. Tags here means a simple text array column while categories refers to a bigint array column that we have configured to act as a foreign key to the categories tables.

Since array columns cannot be foreign keys in Postgres we have to add a simple config to Super Graph to set this up.

tables:
- name: products
columns:
- name: category_ids
related_to: categories.id
...
{
"data": {
"email": "alien@antfarm.com",
"full_name": "aliens",
"created_at": "now",
"updated_at": "now",
"product": {
"name": "Bug Spray",
"tags": ["bad bugs", "be gone"],
"categories": {
"connect": { "id": 1 }
},
"created_at": "now",
"updated_at": "now"
}
}
}
mutation {
user(insert: $data) {
id
product {
id
name
tags
category {
id
name
}
}
}
}

Realtime subscriptions

This is one of the coolest features of Super Graph. It is a highly scalable way to get updates from the database as it updates. Below we use subscriptions to fetch the latest purchases from the database.

{
"cursor": null
}
subscription {
purchases(first: 5, after: $cursor) {
product {
id
name
}
customer {
id
email
}
}
}

Production secrets management

We recommend you use Mozilla SOPS for secrets management. The sops binary is installed on the Super Graph app docker image. To use SOPS you create a yaml file with your secrets like the one below. You then need a secret key to encrypt it. Your options are to go with Google Cloud KMS, Amazon KMS, Azure Key Vault, etc. In production SOPS will automatically fetch the key from your defined KMS, decrypt the secrets file and make the values available to Super Graph via enviroment variables.

  1. Create the secrets file
SG_DATABASE_PASSWORD: postgres
SG_AUTH_JWT_SECRET: jwt_token_secret_key
SG_SECRET_KEY: generic_secret_ke
  1. Login to your cloud (Google Example)
gcloud auth login
gcloud auth application-default login
  1. Encrypt the secrets with the key
sops -e -i ./config/prod.secrets.yml