Intro

I've had this idea for a martial arts training app for a while and I'm finally getting around to really making it. So I figured I'd document my journey. I've been studying martial arts at Plus One Defence in West Hartford for a few years now. In the style I'm working on, there are tests. Sadly, there's a part of the tests I'm pretty bad at... Shadow boxing. Basically the instructor calls out a variety of strikes and you have to do them. But, there are often 4, 5, 6, or even 7 strikes to remember in the patterns they call out. Not too bad if you're fresh, but in the higher ranks things get complicated.

So here's the kind of app I want to make. Minimally, it should:

  • allow people to create an account
  • users should then be able to Create/Read/Update/Delete techniques (e.g. a punch or kick)
  • users should be able to combine those techniques into patterns (more CRUD)
  • Those patterns should be able to be "played" almost like flashcards

The last part would be very similar to what occurs on the test.

To do this, I've chosen a few frameworks to build with:

Before I go any further, a warning.

This set of posts is going to be at an intermediate or advanced level. I will do my best to explain and document as best I can. However, knowledge of the following will be a huge help if you want to understand what's going on.

  • GraphQL
  • React
  • MySQL databases
  • Asynchronous javascript
  • etc...

You can follow my progress in the github repo as well

With that, let's move on to...

The setup

I started by creating a new next js project as described in their documentation.

npx create-next-app --use-npm

The create-next-app command will bootstrap a new next JS project. Instead of starting with the frontend though, I want to address the server and API first.

To begin, install prisma as a dev dependency.

npm install -D prisma

Followed by a bunch of dependencies needed for the server.

npm install apollo-server-micro bcrypt cookies date-fns dotenv graphql graphql-type-json jsonwebtoken micro-cors uuid

We'll get to how all these are used over time. For now though, it's sufficient to say that they will help you create a graphql API which uses JWT authentication.

After the dependencies are installed, it's time to set up prisma.

prisma JS

To start with, you need to make sure that you have an SQL database available. Prisma supports: PostgreSQL, MySQL, SQLite, and SQL Server. How you do that is up to you. I have MySQL installed locally on my Mac through homebrew.

Make sure that the database is running and then cd into your project. Next, it's time to initialize the project for prisma. You can do this with the following command.

npx prisma init

The prisma init command will bootsrap your project by creating a /prisma directory at the root of the project. Inside you will find initially a schema.prisma file. This file is where we'll define models for the database. We'll then map these to the server later. The prisma init command will also create a .env file. This is where you'll define the database credentials prisma needs to run. For local development, I replaced the DATABASE_URL provided with this one:

DATABASE_URL="mysql://root:root@localhost:3306/martial-arts-trainer"

You'll notice that this environment variable is referenced in the schema.prisma file. If you're using MySQL, you'll also need to change the provider value in the datasource. So your schema.prisma file should look like this:

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

Creating a User Model

Now, it's time to create a database model for users. To do that, we need to do a few things:

  • create the columns the users table will have
  • assign an ID to one of the columns
  • ensure that some of the values have default values
  • ensure that some of the values are unique
  • (optionally) name the table
model User {
  id Int @default(autoincrement()) @id
  uuid String @unique
  name String? @default("")
  userName String? @unique
  email String @unique
  password String
  @@map(name: "users")
}

Most of this should be fairly obvious. However, there are a few things to note. The first is that all values are assumed to be required. The way to override that is with the ? after the type. That's why the password property is set as password String while the name property is set as name String?. The second thing to note is that there are essentially two IDs. The first is the id which MySQL will autoincrement. The second is the uuid which needs to be unique to each user. This way, we have options on the server and frontend about which ID to use. We might want to route people to their profile pages based on a UUID for instance instead of a "plain" number. The last thing to notice is the @@map directive. This will tell prisma to name the created table users instead of User. I prefer the plural in this case since we (hopefully) will have more than one user.

Once this is set, you can migrate the model to the database with the prisma migrate command.

prisma migrate dev --preview-feature

According to the prisma documentation, this will do three things.

  1. Detect changes in the prisma.schema file
  2. Create a set of local .sql files
  3. Send the changes to the database

If all has gone correctly, when you refresh your database (I like Sequel Pro as a database app) you should see a users table with columns which match up to the properties defined in the model.

From here, it's time to start setting up an API using Next JS.

NextJS and a GraphQL API

NextJS has a really nice feature. When you bootstrap your app, it'll create a /pages directory. Any file in the /pages directory which exports a function will create a route. So if you create a file at /pages/about.js and export a React component, you'll get a page at yourURL/about. The same thing works for APIs.

So, creating an API route is as quick as having the following directory structure

/pages
|_ /api
  |_ graphql.js

This will eventually let you access yourURL/api/graphql. Now comes the (slightly) tricky part. Creating an apollo server and connecting prisma to it.

Let me show you the file at this point, and then walk through it. I've based it on the example repo provided by NextJS.

import { ApolloServer } from 'apollo-server-micro'
import Cors from 'micro-cors'

import typeDefs from '../../graphql/typeDefs'

const resolvers = {}

const cors = Cors({
  allowMethods: ["GET", "POST", "OPTIONS"]
})

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
})

export const config = {
  api: {
    bodyParser: false
  }
}

const handler = apolloServer.createHandler({
  path: '/api/graphql',
})

export default cors(handler)

In order not to make this file ridiculously long, I moved the type definitions to their own file. I put this at /graphql/typeDefs.js

import { gql } from 'apollo-server-micro'

const typeDefs = gql``

export default typeDefs

At this point, you've got an API that will do absolutely nothing. But, I'd like to walk through what some of the parts (will) do.

Starting with the end in mind, I want to talk about the type definitions file.

typeDefs

In the prisma.schema model, we defined what properties the users table would have. Here, we're going to define what there server knows about. This can include things in the database as well as other things the database doesn't need to know about.

For instance, let's take the User model again. But let's say the user model only defines one field, name.

import { gql } from 'apollo-server-micro'

const typeDefs = gql`
  type User {
    name: String
  }
`

export default typeDefs

If you define the user like this, even though all the other columns in the database exist, you'll never be able to access them. The only column that will "exist" from the point of view of the server or client will be the name column. Obviously this is silly. But, if you never wanted to expose the ID or password to the client or server, you wouldn't have to. Let's add some more properties. Note that the ! marks mean that those properties can not be null.

import { gql } from 'apollo-server-micro'

const typeDefs = gql`
  type User {
    id: Int!
    uuid: String!
    name: String
    userName: String
    email: String!
    password: String!
  }
`

export default typeDefs

Next, we need to create some graphql queries and mutations. These will need to match up with the resolvers property in the server.

import { gql } from 'apollo-server-micro'

type Query {
  users: [User]!
  me: User
}

type Mutation {
  login(credentials: AuthInput!): User!
  createUser(userData: UserInput!): User!
  updateUser(uuid: String! userData: UserInput): User
  deleteUser(uuid: String!): User
}

const typeDefs = gql`
  type User {
    id: Int!
    uuid: String!
    name: String
    userName: String
    email: String!
    password: String!
  }
`

export default typeDefs

Note that in the login and createUser mutations, I've chosen to use AuthInput and UserInput respectively as arguments. This is a more flexible approach than defining each possible argument separately. Those inputs are defined as follows

input AuthUserInput {
  email: String
  userName: String
}

input AuthInput {
  user: AuthUserInput!
  password: String!
}

input UserInput {
  email: String!
  name: String
  userName: String
  password: String!
}

The AuthUserInput will give users the flexibility to login with their choice of either email address or username.

So at this point the whole typeDefs file looks like this.

import { gql } from 'apollo-server-micro'

const typeDefs = gql`
  type Query {
    users: [User]!
    me: User
  }
  type Mutation {
    login(credentials: AuthInput!): User!
    createUser(userData: UserInput!): User!
    updateUser(uuid: String! userData: UserInput): User
    deleteUser(uuid: String!): User
  }
  input AuthUserInput {
    email: String
    userName: String
  }
  input AuthInput {
    user: AuthUserInput!
    password: String
  }
  input UserInput {
    email: String
    name: String
    userName: String
    password: String
  }
  type User {
    id: Int!
    uuid: String!
    name: String
    userName: String
    email: String!
    password: String!
  }
`

export default typeDefs

Now, let's go back to the apollo server implementation.

Resolvers

The resolvers are where we manage the data flow between the client and the database. Queries will get information and mutation will do everything else. For the moment, I'm going to wait to get into the details of the resolvers until the next post. Howerver, what I'll say now is that for every Mutation or Query you define in the type definitions, you need to match it exactly to a resolver.

Context

One thing I haven't addressed yet is the server context. It's another configuration option for the ApolloServer object. While not strictly necessary, it is extremely important. The context property is where I'll handle authentication and providing access to cookies. It's a function that takes in the server request and response objects as arguments. It then returns an object. Anything added to the returned context object will be available in our resolvers. That's extremely useful and I'll go over how to do this in the next post. For now, it's going to look like this:

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req, res }) => {
    return {}
  }
}) 

Conclusion

At this point, we're in a good position to start what I think is the most difficult part of the application. In the next post, I'll show how to: create a new user, log that user in with JWT tokens, and get that user's information from the database. Until then, I hope you enjoyed this post!