- edb-alt-title
Building a simple blog application with EdgeDB and Next.js (App Router)
We're going to build a simple blog application with Next.js and EdgeDB. Let's start by scaffolding our app with Next.js's create-next-app
tool.
You'll be prompted to provide a name (we'll use nextjs-blog
) for your app and choose project options. For this tutorial, we'll go with the recommended settings including TypeScript, App Router, and opt-ing out of the src/
directory.
$ npx create-next-app@latest
✔ Would you like to use TypeScript? Yes
✔ Would you like to use ESLint? Yes
✔ Would you like to use Tailwind CSS? Yes
✔ Would you like to use src/ directory? No
✔ Would you like to use App Router? (recommended) Yes
✔ Would you like to customize the default import alias (@/*) Yes
The scaffolding tool will create a simple Next.js app and install its dependencies. Once it's done, you can navigate to the app's directory and start the development server.
$ cd nextjs-blog
$ npm dev # or yarn dev or pnpm dev or bun run dev
When the dev server starts, it will log out a local URL. Visit that URL to see the default Next.js homepage. At this point the app's file structure looks like this:
README.md
tsconfig.json
package.json
next.config.js
next-env.d.ts
postcss.config.js
tailwind.config.js
app
├── page.tsx
├── layout.tsx
├── globals.css
└── favicon.ico
public
├── next.tsx
└── vercel.svg
There's an async function Home
defined in app/page.tsx
that renders the homepage. It's a Server Component <https://nextjs.org/docs/app/building-your-application/ rendering/server-components> which lets you integrate server-side logic directly into your React components. Server Components are executed on the server and can fetch data from a database or an API. We'll use this feature to load blog posts from an EdgeDB database.
Let's start by implementing a simple homepage for our blog application using static data. Replace the contents of app/page.tsx
with the following.
import Link from 'next/link'
type Post = {
id: string
title: string
content: string
}
export default async function Home() {
const posts: Post[] = [
{
id: 'post1',
title: 'This one weird trick makes using databases fun',
content: 'Use EdgeDB',
},
{
id: 'post2',
title: 'How to build a blog with EdgeDB and Next.js',
content: "Let's start by scaffolding our app with `create-next-app`.",
},
]
return (
<div className="container mx-auto p-4 bg-black text-white">
<h1 className="text-3xl font-bold mb-4">Posts</h1>
<ul>
{posts.map((post) => (
<li
key={post.id}
className="mb-4"
>
<Link
href={`/post/${post.id}`}
className="text-blue-500"
>
{post.title}
</Link>
</li>
))}
</ul>
</div>
)
}
After saving, you can refresh the page to see the blog posts. Clicking on a post title will take you to a page that doesn't exist yet. We'll create that page later in the tutorial.
Now let's spin up a database for the app. First, install the edgedb
CLI.
Linux or macOS
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.edgedb.com | sh
Windows Powershell
PS> iwr https://ps1.edgedb.com -useb | iex
Then check that the CLI is available with the edgedb --version
command. If you get a Command not found
error, you may need to open a new terminal window before the edgedb
command is available.
Once the CLI is installed, initialize a project from the application's root directory. You'll be presented with a series of prompts.
$ edgedb project init
No `edgedb.toml` found in `~/nextjs-blog` or above
Do you want to initialize a new project? [Y/n]
> Y
Specify the name of EdgeDB instance to use with this project [default:
nextjs_blog]:
> nextjs_blog
Checking EdgeDB versions...
Specify the version of EdgeDB to use with this project [default: x.x]:
>
┌─────────────────────┬──────────────────────────────────────────────┐
│ Project directory │ ~/nextjs-blog │
│ Project config │ ~/nextjs-blog/edgedb.toml │
│ Schema dir (empty) │ ~/nextjs-blog/dbschema │
│ Installation method │ portable package │
│ Start configuration │ manual │
│ Version │ x.x │
│ Instance name │ nextjs_blog │
└─────────────────────┴──────────────────────────────────────────────┘
Initializing EdgeDB instance...
Applying migrations...
Everything is up to date. Revision initial.
Project initialized.
This process has spun up an EdgeDB instance called nextjs_blog
and associated it with your current directory. As long as you're inside that directory, CLI commands and client libraries will be able to connect to the linked instance automatically, without additional configuration.
To test this, run the edgedb
command to open a REPL to the linked instance.
$ edgedb
EdgeDB x.x (repl x.x)
Type \help for help, \quit to quit.
edgedb> select 2 + 2;
{4}
>
From inside this REPL, we can execute EdgeQL queries against our database. But there's not much we can do currently, since our database is schemaless. Let's change that.
The project initialization process also created a new subdirectory in our project called dbschema
. This is folder that contains everything pertaining to EdgeDB. Currently it looks like this:
dbschema
├── default.esdl
└── migrations
The default.esdl
file will contain our schema. The migrations
directory is currently empty, but will contain our migration files. Let's update the contents of default.esdl
with the following simple blog schema.
module default {
type BlogPost {
required title: str;
required content: str {
default := ""
}
}
}
Note
EdgeDB lets you split up your schema into different modules
but it's common to keep your entire schema in the default
module.
Save the file, then let's create our first migration.
$ edgedb migration create
did you create object type 'default::BlogPost'? [y,n,l,c,b,s,q,?]
> y
Created ./dbschema/migrations/00001.edgeql
The dbschema/migrations
directory now contains a migration file called 00001.edgeql
. Currently though, we haven't applied this migration against our database. Let's do that.
$ edgedb migrate
Applied m1fee6oypqpjrreleos5hmivgfqg6zfkgbrowx7sw5jvnicm73hqdq (00001.edgeql)
Our database now has a schema consisting of the BlogPost
type. We can create some sample data from the REPL. Run the edgedb
command to re-open the REPL.
$ edgedb
EdgeDB 4.x (repl 4.x)
Type \help for help, \quit to quit.
edgedb>
Then execute the following insert
statements.
edgedb> insert BlogPost {
....... title := "This one weird trick makes using databases fun",
....... content := "Use EdgeDB"
....... };
{default::BlogPost {id: 7f301d02-c780-11ec-8a1a-a34776e884a0}}
edgedb> insert BlogPost {
....... title := "How to build a blog with EdgeDB and Next.js",
....... content := "Let's start by scaffolding our app..."
....... };
{default::BlogPost {id: 88c800e6-c780-11ec-8a1a-b3a3020189dd}}
Now that we have a couple posts in the database, let's load them into our Next.js app. To do that, we'll need the edgedb
client library. Let's install that from NPM:
$ npm install edgedb
# or yarn add edgedb or pnpm add edgedb or bun add edgedb
Then go to the app/page.tsx
file to replace the static data with the blogposts fetched from the database.
To fetch these from the homepage, we'll create an EdgeDB client and use the .query()
method to fetch all the posts in the database with a select
statement.
import Link from 'next/link'
import { createClient } from 'edgedb';
- type Post = {
id: string title: string content: string
}
const client = createClient();
export default async function Home() {
- const posts: Post[] = [
- {
- id: 'post1',
- title: 'This one weird trick makes using databases fun',
- content: 'Use EdgeDB',
- },
- {
- id: 'post2',
- title: 'How to build a blog with EdgeDB and Next.js',
- content: "Start by scaffolding our app with create-next-app.",
- },
- ]
- const posts = await client.query<Post>(`
- select BlogPost {
- id,
- title,
- content
+ };`)
- return (
- <div className="container mx-auto p-4 bg-black text-white">
<h1 className="text-3xl font-bold mb-4">Posts</h1> <ul> {posts.map((post) => ( <li key={post.id} className="mb-4" > <Link href={/post/${post.id}} className="text-blue-500" > {post.title} </Link> </li> ))} </ul>
</div>
)
}
When you refresh the page, you should see the blog posts.
Since we're using TypeScript, it makes sense to use EdgeDB's powerful query builder. This provides a schema-aware client API that makes writing strongly typed EdgeQL queries easy and painless. The result type of our queries will be automatically inferred, so we won't need to manually type something like type Post = { id: string; ... }
.
First, install the generator to your project.
$ npm install --save-dev @edgedb/generate
$ # or yarn add --dev @edgedb/generate
$ # or pnpm add --dev @edgedb/generate
$ # or bun add --dev @edgedb/generate
Then generate the query builder with the following command.
$ npx @edgedb/generate edgeql-js
Generating query builder...
Detected tsconfig.json, generating TypeScript files.
To override this, use the --target flag.
Run `npx @edgedb/generate --help` for full options.
Introspecting database schema...
Writing files to ./dbschema/edgeql-js
Generation complete! 🤘
Checking the generated query builder into version control
is not recommended. Would you like to update .gitignore to ignore
the query builder directory? The following line will be added:
dbschema/edgeql-js
[y/n] (leave blank for "y")
> y
This command introspected the schema of our database and generated some code in the dbschema/edgeql-js
directory. It also asked us if we wanted to add the generated code to our .gitignore
; typically it's not good practice to include generated files in version control.
Back in app/page.tsx
, let's update our code to use the query builder instead.
import Link from 'next/link'
import { createClient } from 'edgedb';
- import e from '@/dbschema/edgeql-js';
- type Post = {
- id: string
- title: string
- content: string
} const client = createClient();
export default async function Home() {
- const posts = await client.query(`
- select BlogPost {
- id,
- title,
- content
- };`)
- const selectPosts = e.select(e.BlogPost, () => ({
- id: true,
- title: true,
- content: true,
- }));
+ const posts = await selectPosts.run(client);
- return (
- <div className="container mx-auto p-4 bg-black text-white">
<h1 className="text-3xl font-bold mb-4">Posts</h1> <ul> {posts.map((post) => ( <li key={post.id} className="mb-4" > <Link href={/post/${post.id}} className="text-blue-500" > {post.title} </Link> </li> ))} </ul>
</div>
)
}
Instead of writing our query as a plain string, we're now using the query builder to declare our query in a code-first way. As you can see, we import the query builder as a single default import e
from the dbschema/edgeql-js
directory.
Now, when we update our selectPosts
query, the type of our dynamically loaded posts
variable will update automatically — no need to keep our type definitions in sync with our API logic!
Our homepage renders a list of links to each of our blog posts, but we haven't implemented the page that actually displays the posts. Let's create a new page at app/post/[id]/page.tsx
. This is a dynamic route <https://nextjs.org/docs/app/building-your-application/ routing/dynamic-routes> that includes an id
URL parameter. We'll use this parameter to fetch the appropriate post from the database.
Add the following code in app/post/[id]/page.tsx
:
import { createClient } from 'edgedb'
import e from '@/dbschema/edgeql-js'
import Link from 'next/link'
const client = createClient()
export default async function Post({ params }: { params: { id: string } }) {
const post = await e
.select(e.BlogPost, (post) => ({
id: true,
title: true,
content: true,
filter_single: e.op(post.id, '=', e.uuid(params.id)),
}))
.run(client)
if (!post) {
return <div>Post not found</div>
}
return (
<div className="container mx-auto p-4 bg-black text-white">
<nav>
<Link
href="/"
className="text-blue-500 mb-4 block"
replace
>
Back to list
</Link>
</nav>
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<p>{post.content}</p>
</div>
)
}
We are again using a Server Component to fetch the post from the database. This time, we're using the filter_single
method to filter the BlogPost
type by its id
. We're also using the uuid
function from the query builder to convert the id
parameter to a UUID.
Now, click on one of the blog post links on the homepage. This should bring you to /post/<uuid>
.
You can deploy an EdgeDB instance on the EdgeDB Cloud or on your preferred cloud provider. We'll cover both options here.
#1 Deploy EdgeDB
First, sign up for an account at cloud.edgedb.com and create a new instance. Create and make note of a secret key for your EdgeDB Cloud instance. You can create a new secret key from the "Secret Keys" tab in the EdgeDB Cloud console. We'll need this later to connect to the database from Vercel.
Run the following command to migrate the project to the EdgeDB Cloud:
$ edgedb migrate -I <org>/<instance-name>
Note
Alternatively, if you want to restore your data from a local instance to the cloud, you can use the edgedb dump
and edgedb restore
commands.
$ edgedb dump <your-dump.dump>
$ edgedb restore -I <org>/<instance-name> <your-dump.dump>
The migrations and schema will be automatically applied to the cloud instance.
#2 Set up a `prebuild` script
Add the following prebuild
script to your package.json
. When Vercel initializes the build, it will trigger this script which will generate the query builder. The npx @edgedb/generate edgeql-js
command will read the value of the EDGEDB_SECRET_KEY
and EDGEDB_INSTANCE
variables, connect to the database, and generate the query builder before Vercel starts building the project.
// package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
- + "prebuild": "npx @edgedb/generate edgeql-js"
},
#3 Deploy to Vercel
Push your project to GitHub or some other Git remote repository. Then deploy this app to Vercel with the button below.
In "Configure Project," expand "Environment Variables" to add two variables:
EDGEDB_INSTANCE
containing your EdgeDB Cloud instance name (in<org>/<instance-name>
format)EDGEDB_SECRET_KEY
containing the secret key you created and noted previously.
#4 View the application
Once deployment has completed, view the application at the deployment URL supplied by Vercel.
#1 Deploy EdgeDB
Check out the following guides for deploying EdgeDB to your preferred cloud provider:
- AWS
- Google Cloud
- Azure
- DigitalOcean
- Fly.io
- Docker (cloud-agnostic)
#2 Find your instance's DSN
The DSN is also known as a connection string. It will have the format edgedb://username:password@hostname:port
. The exact instructions for this depend on which cloud you are deploying to.
#3 Apply migrations
Use the DSN to apply migrations against your remote instance.
$ edgedb migrate --dsn <your-instance-dsn> --tls-security insecure
Note
You have to disable TLS checks with --tls-security insecure
. All EdgeDB instances use TLS by default, but configuring it is out of scope of this project.
Once you've applied the migrations, consider creating some sample data in your database. Open a REPL and insert
some blog posts:
$ edgedb --dsn <your-instance-dsn> --tls-security insecure
EdgeDB x.x (repl x.x)
Type \help for help, \quit to quit.
edgedb> insert BlogPost { title := "Test post" };
{default::BlogPost {id: c00f2c9a-cbf5-11ec-8ecb-4f8e702e5789}}
#4 Set up a `prebuild` script
Add the following prebuild
script to your package.json
. When Vercel initializes the build, it will trigger this script which will generate the query builder. The npx @edgedb/generate edgeql-js
command will read the value of the EDGEDB_DSN
variable, connect to the database, and generate the query builder before Vercel starts building the project.
// package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
- + "prebuild": "npx @edgedb/generate edgeql-js"
},
#5 Deploy to Vercel
Deploy this app to Vercel with the button below.
When prompted:
- Set
EDGEDB_DSN
to your database's DSN - Set
EDGEDB_CLIENT_TLS_SECURITY
toinsecure
. This will disable EdgeDB's default TLS checks; configuring TLS is beyond the scope of this tutorial.
#6 View the application
Once deployment has completed, view the application at the deployment URL supplied by Vercel.
This tutorial demonstrates how to work with EdgeDB in a Next.js app, using the App Router. We've created a simple blog application that loads posts from a database and displays them on the homepage. We've also created a dynamic route that fetches a single post from the database and displays it on a separate page.
The next step is to add a /newpost
page with a form for writing new blog posts and saving them into EdgeDB. That's left as an exercise for the reader.
To see the final code for this tutorial, refer to github.com/edgedb/edgedb-examples/tree/main/nextjs-blog <https://github.com/edgedb/edgedb-examples/tree/main/ nextjs-blog-app-router>.