Setup
Creating a Next.js Project
First, we need to create a new Next.js Project. Here, we simply follow the steps from the Next.js chapter.
Run the following command to create a new Next.js project:
pnpm create next-app
Give your project the name easy-opus
and select the following options:
- we want to use TypeScript
- we want to use ESLint
- we want to use Tailwind CSS
- we want to use the
src/
directory - we want to use the App Router
- we don't want to customize the defalt import alias
Note that from now on we specify all paths relative to the src
directory.
For example if we refer to a file thingy/example.ts
that file will actually be in src/thingy/example.ts
.
If you're unsure about the location of a file, you can also look at the end of this section, which contains the file tree you should have after the setup is completed.
Removing Unneccessary Code
Let's remove all the unneccessary code from the generated files.
Change the file app/layout.tsx
to look like this:
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Easy Opus',
description: 'A simple task management application',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Change the file app/page.tsx
to look like this:
export default function Home() {
return <h1 className="underline">Welcome to easy-opus</h1>;
}
Change the file app/globals.css
to look like this:
@tailwind base;
@tailwind components;
@tailwind utilities;
Additionally, feel free to delete the SVG files in the public
directory and to change (or delete) the favicon
.
Run pnpm dev
and check out the page at http://localhost:3000
.
You should see the underlined text Welcome to easy-opus
.
Setup a Database
Next we need to setup our database. To accomplish this, we will simply follow the steps from the SQL chapter.
Create a new Supabase project, copy the database URL and create the following .env.local
file:
DATABASE_URL=$YOUR_DATABASE_URL_HERE
Of course, you need to specify the actual database URL you copied from Supabase instead of
$YOUR_DATABASE_URL_HERE
.
Setup Drizzle
Next, we need to set up Drizzle. Here we will simply follow the steps from the Drizzle chapter.
Install Drizzle and dotenv
:
pnpm add drizzle-orm postgres dotenv
pnpm add --save-dev tsx drizzle-kit
Create a new directory called db
.
This is where our database-related files will go.
Remember that we specify all paths relative to
src
, i.e. you need to create thedb
directory insrc
.
Now create a directory db/migrations
to store the migrations.
Create a file db/drizzle.config.ts
:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './src/db/schema.ts',
out: './src/db/migrations',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
Next we create a file db/migrate.ts
:
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import dotenv from 'dotenv';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
dotenv.config({ path: ['.env.local', '.env'] });
const databaseURI = process.env.DATABASE_URL;
if (databaseURI === undefined) {
console.log('You need to provide the database URI');
process.exit(0);
}
const client = postgres(databaseURI, { max: 1 });
const db = drizzle(client);
async function runMigrations() {
await migrate(db, { migrationsFolder: './src/db/migrations' });
await client.end();
}
runMigrations().then(console.log).catch(console.error);
Finally, let's create the initial schema at db/schema.ts
:
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const projectTable = pgTable('project', {
id: serial('id').primaryKey(),
userId: text('user_id').notNull(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
To simplify migrations, we will add the following scripts to package.json
:
{
"scripts": {
// other scripts
"db:generate": "pnpm drizzle-kit generate --config=src/db/drizzle.config.ts",
"db:migrate": "pnpm tsx src/db/migrate.ts"
}
}
Now run pnpm db:generate
to generate the migration.
Inspect the migration (which would be something like db/migrations/0000_curious_vanisher-sql
) and make sure that it contains the right content:
CREATE TABLE IF NOT EXISTS "project" (
"id" serial PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
Run pnpm db:migrate
to apply the migration to the database.
Verify that your database contains a project
table with the right columns.
Finally, we create the db/index.ts
file which exports the db
object to allow other files to call database functions:
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const databaseURL = process.env.DATABASE_URL!;
const client = postgres(databaseURL);
export const db = drizzle(client);
Yes, this subsection was essentially a repeat of things you already learned in the Drizzle chapter.
Linting
If you look through the scripts in package.json
, you will see a curious little script called lint
that executes next lint
.
This scripts provides an integrated ESLint experience. ESLint is an awesome tool that statically analyzes your code to quickly find problems.
Note that ESLint is not for finding syntax or type errors (your TypeScript compiler already takes care of that). Instead it has a lot of rules for good code and bad code and attempts to help you with writing high-quality code.
Let's run it:
pnpm lint
Unless you messed something up, this should output:
✔ No ESLint warnings or errors
Great! Currently, ESLint has nothing to tell us.
File Structure
This is the file structure you should have right now:
├── .env.local
├── .eslintrc.json
├── next.config.mjs
├── next-env.d.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── README.md
├── src
│ ├── app
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ └── db
│ ├── drizzle.config.ts
│ ├── index.ts
│ ├── migrate.ts
│ ├── migrations
│ │ ├── 0000_curious_vanisher.sql
│ │ └── meta
│ │ ├── 0000_snapshot.json
│ │ └── _journal.json
│ └── schema.ts
├── tailwind.config.ts
└── tsconfig.json
You should absolutely understand each and every one of these files if you've read the book carefully.
Just to recap:
The README.md
file contains basic information about the project.
The package.json
file marks the directory as a JavaScript project and contains vital project information such as the name, the dependencies and the scripts of this project.
The pnpm-lock.yaml
file is automatically generated by the pnpm
package manager and contains a complete list of all dependencies (including nested dependencies).
The node_modules
contain the actual dependencies.
The tsconfig.json
file marks the directory as a TypeScript project and primarily contains important compiler options for the TypeScript compiler.
The next.config.mjs
file contains the configuration that is relevant for Next.js.
The next-env.d.ts
file ensures that Next.js types are picked up by the TypeScript compiler.
The tailwind.config.ts
file contains the configuration that is relevant for Tailwind CSS.
The postcss.config.js
file contains the configuration relevant for PostCSS (which is used by Tailwind CSS).
The file src/app/page.tsx
specifies the root page and src/app/layout.tsx
specifies the root layout.
The globals.css
file specifies global styles - right we only really need it for the Tailwind directives.
The src/db
directory contains everything that is related to the database (including the migrations).
The .eslintrc.json
contain the eslint
configuration.
The .env.local
file contains our enviroment variables.