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 want to use Turbopack for
next dev
- we don't want to customize the default import alias
Note that from now on we specify all paths relative to the src/app
directory.
For example if we refer to a file thingy/example.ts
that file will actually be in src/app/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 Unnecessary Code
Let's remove all the unnecessary code from the generated files.
Change the file layout.tsx
to look like this:
import type { Metadata } from 'next';
import './globals.css';
import Link from 'next/link';
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>
<nav className="bg-blue-600 bg-opacity-70 text-white p-4 shadow-md flex items-center justify-between">
<div className="flex justify-center w-full">
<Link href="/" className="text-lg font-bold">
easy-opus
</Link>
</div>
</nav>
<>{children}</>
</body>
</html>
);
}
Change the file 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 the favicon.ico
.
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
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
.
Remember that if your password has special characters like :
or /
, you will need to replace them with their respective percent-encodings.
Setup Drizzle
Next, we need to set up Drizzle. Here we will simply follow the steps from the Drizzle chapter.
Install drizzle-orm
and pg
:
pnpm add drizzle-orm pg
pnpm add --save-dev tsx drizzle-kit
Also, install the @types/pg
package to get the type definitions for pg
:
pnpm add @types/pg --save-dev
Finally, install the drizzle-kit
package as a dev dependency:
pnpm add drizzle-kit --save-dev
Create a new directory called db
.
This is where our database-related files will go.
You should also create a directory db/migrations
where we will store the migrations.
Remember that we specify all paths relative to
src/app
, i.e. you need to create thedb
directory insrc/app
.
Create a file db/drizzle.config.ts
:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './src/app/db/migrations',
schema: './src/app/db/schema.ts',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
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(),
name: text('name').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
To simplify migrations, we will add the generate
and migrate
scripts to package.json
:
{
"scripts": {
// other scripts
"db:generate": "pnpm drizzle-kit generate --config=./src/app/db/drizzle.config.ts",
"db:migrate": "pnpm drizzle-kit migrate"
}
}
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,
"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';
export const db = drizzle(process.env.DATABASE_URL!);
Yes, this subsection was essentially a repeat of things you've 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 script 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 that help you avoid sketchy 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
├── .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
│ ├── 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
file contains our environment variables.