Demo API
This is the recommended starting point if you'd like to see Noun & Verb in action, but don't have an existing Prisma schema.
Step 1: Initialize a demo API module
Open a terminal into a directory that you can create the demo API within.
The easiest way to get started with Noun & Verb is to use it's creator module
❯ npm init noun-and-verb
Since we are trying the demo, just press ENTER when presented with the dialog below.
❯ npm init noun-and-verb
? Would you like to see a demo? (Use arrow keys)
❯ demo
new api
You are then presented with an opportunity to name your API. Just pressing enter will name it noun-and-verb-demo. However you can call it whatever you'd like. This is also used as the npm module name for the API server.
It's a good idea to keep it simple with file & URI safe strings (a-zA-Z_-)
❯ npm init noun-and-verb
? Would you like to see a demo? demo
? What would you like to name your API? (noun-and-verb-demo)
The last question before we are ready to create the starting point, is the directory name for the API. By default, it'll use the same name as the API name. But you can all it something different.
❯ npm init noun-and-verb
? Would you like to see a demo? demo
? What would you like to name your API? noun-and-verb-demo
? Which directory would you like to use? noun-and-verb-demo
The create-noun-and-verb now goes to work for you:
init noun-and-verb (0.0.25) (demo=noun-and-verb-demo)
ℹ Initializing git repository
ℹ Initializing package.json
ℹ Setting up .env file
ℹ Initializing demo models in prisma/schema.prisma
ℹ Installed dependencies (38.7s)
✔ Successfully initialized 'noun-and-verb-demo', a demo noun-and-verb API
This has created a starting point for the demo repo, using a SQLite as the database. This allows you to get started with the minimal fuss.
At this point we are ready to move forward. The command also helpfully prints the next steps. These might seem a bit overwhelming, but worry not! We'll break it down, step-by-step and you'll be a master of this universe in no time at all.
Next steps:
cd noun-and-verb-demo
# TODO: inspect example model in prisma/schema.prisma
npm run generate
export NODE_ENV=dev # protect production DB
npm run migrate # synchronize schema with 'sqlite'
npm run test
npm run dev # starts the dev server
visit http://localhost:1234 to exercise your API!
For more, visit https://NounAndVerb.io
Let's change directory to our API server.
> cd noun-and-verb-demo
Step 2: Inspect Prisma Schema
Now we are ready for a first inspection of the demo Prisma schema.
If you haven't seen a prisma schema before, this might look overwhelming. Prisma Schema documentation is the authoritative source. In short, there are three parts of the Prisma Schema -
- datasource: Defines which database flavor we'd like to use (one per schema)
- generators: Operation on the schema to generate all necessary code. Noun & Verb is implemented as a generator.
- models: These are the database tables and fields that represent your application needs.
To view the demo schema.prisma:
code -n prisma/schema.prisma
Datasource
The Demo API uses a SQLite database, to minimize the fuss involved.
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
Generators
The demo API is setup with two generators:
- prisma-client-js : Responsible for generating the default Prisma client.
- noun-and-ver: Responsible for generating a GraphQL API as advertised.
generator client {
provider = "prisma-client-js"
}
generator noun_and_verb {
// This is the noun & verb generator
// Learn more about it in the docs: https://NounAndVerb.io/docs
provider = "noun_and_verb"
}
Models
models are the raison dêtre of Prisma. They define the database tables, and code is generated around it.
A vanilla Prima model
This is what Prisma requires to generate a client.
model User {
id String @id @default(cuid())
name String
username String
email String
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cart Cart?
}
Noun & Verb Annotations
Noun & Verb adds a few annotations to work it's magic. We'll introduce these step-by-step to keep things sane.
@readOnly Annotation
This is a field level annotation, added to fields that the modifiable on the server, but should never be written to by a client. These fields are missing from any create/update operations.
Typical examples are Ids, createdAt/updatedAt timestamps etc.
model User {
/// @readOnly
id String @id @default(cuid())
name String
username String
email String
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
@writeOnly Annotation
This a field level annotation, added to fields that are writable by the client, but not accessible after. These fields are missing from any read operations.
Typical example of this is a password field.
model User {
/// @readOnly
id String @id @default(cuid())
name String
username String
email String
/// This is a password
/// @writeOnly
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
@createOnly Annotation
A field level annotation, specifies fields that are writable only on creation, but readOnly after. These fields are missing from any update operations.
Typical example of this is a username field.
model User {
/// @readOnly
id String @id @default(cuid())
name String
/// @createOnly
username String
email String
/// This is a password
/// @writeOnly
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
@scalar Annotation
A field level annotation, provides validation of field values, and serialization/deserialization of the value across the wire.
76 different scalars are supported out-of-the-box. It's possible to define your own scalars easily. Please see scalar documentation for details.
model User {
/// @readOnly
id String @id @default(cuid())
name String
/// @createOnly
username String
/// @scalar Email
email String
/// This is a password
/// @writeOnly
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
@seed Annotation
A model level annotation, specifies which models to use as "roots" for seed (synthetic) data generation. Necessary since Relational graphs within a Database are cyclical by nature. The @seed annotation helps specify the "roots" of directed-acyclic-graphs.
Note, that you can have as many seeds as you'd like. For large schemas, it's wise to be selective, since the seed annotation is used to create 100 elements of the root node and depending on your Database connection specifics, this can add up in time.
Please see seed annotation for details.
/// @seed
model User {
/// @readOnly
id String @id @default(cuid())
name String
/// @createOnly
username String
/// @scalar Email
email String
/// This is a password
/// @writeOnly
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
@mock Annotation
A field level annotation, controls the synthetic data generated for a field.
Uses faker.js to provide built in support for these 155 types. It's easy to add custom generators. Please see @mock documentation for more details.
/// @seed
model User {
/// @readOnly
id String @id @default(cuid())
/// @mock faker.name.firstName
name String
/// @createOnly
username String
/// @scalar Email
email String
/// This is a password
/// @writeOnly
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
The complete annotated models
We've just added the minimal set of annotations on the other modes.
/// @seed
model User {
/// @readOnly
id String @id @default(cuid())
/// @mock faker.name.firstName
name String
/// @createOnly
username String
/// @scalar Email
email String
/// This is a password
/// @writeOnly
password String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
cart Cart?
}
model Cart {
/// @readOnly
id String @id @default(cuid())
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
userId String @unique
items Product[]
coupon String?
}
model Product {
/// @readOnly
id String @id @default(cuid())
name String
price Int
image String
/// @readOnly
createdAt DateTime @default(now())
/// @readOnly
updatedAt DateTime @updatedAt
carts Cart[]
}
The workflow
So far, we have only learnt about how to specify our intent via the schema. Now, it's time to use it to deliver value.
When we created the npm module for our API in step 1, we installed all necessary dependencies and setup npm run-scrips. Which are just wrappers around commands we'd need to invoke, with sensible defaults. These are exposed in their raw form, so you have the chance to configure them should the need arise in your situation.
Step 3: Invoke the generators
This command invokes the Prisma cli, and invokes both the generators we learnt about in Step #2.
> npm run generate
> noun-and-verb-demo@0.0.1 generate
> prisma generate && npm run todo
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Noun & Verb:
1. generated GraphQL specs (CRUD)
2. generated Prisma DB seeders
3. generated GraphQL resolvers (CRUD)
4. generated tests
✔ Generated Prisma Client (3.15.2 | library) to ./node_modules/@prisma/client in 44ms
✔ Generated Noun & Verb - GraphQL generator for Prisma to ./ in 1.51s
You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
> noun-and-verb-demo@0.0.1 todo
> npx leasot --ignore '**/*.sdl,**/*.json,**/*.snap' src
✔ No todos/fixmes found