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:
- Next JS for the site and API
- Apollo server and client for graphql support
- Prisma JS as an ORM to connect to a MySQL database
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.
- Detect changes in the
prisma.schema
file - Create a set of local
.sql
files - 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'
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
}
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!