Search
ctrl/
Ask AI
Light
Dark
System

Building a simple blog application with EdgeDB and Next.js (Pages 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. We’ll be using TypeScript for this tutorial.

Copy
$ 
npx create-next-app --typescript nextjs-blog

This will take a minute to run. The scaffolding tool is creating a simple Next.js app and installing all our dependencies for us. Once it’s complete, let’s navigate into the directory and start the dev server.

Copy
$ 
cd nextjs-blog
Copy
$ 
yarn dev

Open localhost:3000 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
pages
├── _app.tsx
├── api
│   └── hello.ts
└── index.tsx
public
├── favicon.ico
└── vercel.svg
styles
├── Home.module.css
└── globals.css

There’s a custom App component defined in pages/_app.tsx that loads some global CSS, plus the homepage at pages/index.tsx and a single API route at pages/api/hello.ts. The styles and public directories contain some other assets.

Let’s start by implementing a simple homepage for our blog application using static data. Replace the contents of pages/index.tsx with the following.

Copy
// pages/index.tsx

import type {NextPage} from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';

type Post = {
  id: string;
  title: string;
  content: string;
};

const HomePage: NextPage = () => {
  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={styles.container}>
      <Head>
        <title>My Blog</title>
        <meta name="description" content="An awesome blog" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Blog</h1>
        <div style={{height: '50px'}}></div>
        {posts.map((post) => {
          return (
            <a href={`/post/${post.id}`} key={post.id}>
              <div className={styles.card}>
                <p>{post.title}</p>
              </div>
            </a>
          );
        })}
      </main>
    </div>
  );
};

export default HomePage;

After saving, Next.js should hot-reload, and the homepage should look something like this.

Basic blog homepage with static content

Now let’s spin up a database for the app. You have two options to initialize an EdgeDB project: using npx edgedb without installing the CLI, or installing the edgedb CLI directly. In this tutorial, we’ll use the first option. If you prefer to install the CLI, see the EdgeDB CLI installation guide for more information.

From the application’s root directory, run the following command:

Copy
$ 
npx 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 “linked” 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.

Copy
$ 
edgedb
EdgeDB 2.x (repl 2.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.

Copy
# dbschema/default.esdl

module default {
  type BlogPost {
    required property title -> str;
    required property content -> str {
      default := ""
    };
  }
}

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.

Copy
$ 
npx 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.

Copy
$ 
npx 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.

Copy
$ 
edgedb
EdgeDB 2.x (repl 2.x)
Type \help for help, \quit to quit.
edgedb>

Then execute the following insert statements.

Copy
edgedb> 
....... 
....... 
....... 
insert BlogPost {
  title := "This one weird trick makes using databases fun",
  content := "Use EdgeDB"
};
{default::BlogPost {id: 7f301d02-c780-11ec-8a1a-a34776e884a0}}
Copy
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 dynamically with a Next.js API route. To do that, we’ll need the edgedb client library. Let’s install that from NPM:

Copy
$ 
npm install edgedb

Then create a new file at pages/api/post.ts and copy in the following code.

Copy
// pages/api/post.ts

import type {NextApiRequest, NextApiResponse} from 'next';
import {createClient} from 'edgedb';

export const client = createClient();

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const posts = await client.query(`select BlogPost {
    id,
    title,
    content
  };`);
  res.status(200).json(posts);
}

This file initializes an EdgeDB client, which manages a pool of connections to the database and provides an API for executing queries. We’re using the .query() method to fetch all the posts in the database with a simple select statement.

If you visit localhost:3000/api/post in your browser, you should see a plaintext JSON representation of the blog posts we inserted earlier.

To fetch these from the homepage, we’ll use useState, useEffect, and the built-in fetch API. At the top of the HomePage component in pages/index.tsx, replace the static data and add the missing imports.

Copy
 // pages/index.tsx
 import {useState, useEffect} from 'react';

 type Post = {
   id: string;
   title: string;
   content: string;
 };

 const HomePage: NextPage = () => {
   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...",
     },
   ];

   const [posts, setPosts] = useState<Post[] | null>(null);
   useEffect(() => {
     fetch(`/api/post`)
       .then((result) => result.json())
       .then(setPosts);
   }, []);
   if (!posts) return <p>Loading...</p>;

   return <div>...</div>;
 }

When you refresh the page, you should briefly see a Loading... indicator before the homepage renders the (dynamically loaded!) 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.

Copy
$ 
yarn add --dev @edgedb/generate

Then generate the query builder with the following command.

Copy
$ 
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 pages/api/post.ts, let’s update our code to use the query builder instead.

Copy
// pages/api/post.ts

import type {NextApiRequest, NextApiResponse} from 'next';
import {createClient} from 'edgedb';
import e, {$infer} from '../../dbschema/edgeql-js';

export const client = createClient();

const selectPosts = e.select(e.BlogPost, () => ({
  id: true,
  title: true,
  content: true,
}));

export type Posts = $infer<typeof selectPosts>;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const posts = await client.query(`select BlogPost {
    id,
    title,
    content
  };`);
  const posts = await selectPosts.run(client);
  res.status(200).json(posts);
}

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.

We’re also using a utility called $infer to extract the inferred type of this query. In VSCode you can hover over Posts to see what this type is.

Inferred type of posts query

Back in pages/index.tsx, let’s update our code to use the inferred Posts type instead of our manual type declaration.

Copy
 // pages/index.tsx

 import type {NextPage} from 'next';
 import Head from 'next/head';
 import {useEffect, useState} from 'react';
 import styles from '../styles/Home.module.css';
 import {Posts} from "./api/post";

 type Post = {
   id: string;
   title: string;
   content: string;
 };

 const Home: NextPage = () => {

   const [posts, setPosts] = useState<Posts | null>(null);
   // ...

 }

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 pages/post/[id].tsx. This is a dynamic route that includes an id URL parameter. We’ll use this parameter to fetch the appropriate post from the database.

Create pages/post/[id].tsx and add the following code. We’re using getServerSideProps to load the blog post data server-side, to avoid loading spinners and ensure the page loads fast.

Copy
import React from 'react';
import {GetServerSidePropsContext, InferGetServerSidePropsType} from 'next';

import {client} from '../api/post';
import e from '../../dbschema/edgeql-js';

export const getServerSideProps = async (
  context?: GetServerSidePropsContext
) => {
  const post = await e
    .select(e.BlogPost, (post) => ({
      id: true,
      title: true,
      content: true,
      filter_single: e.op(
        post.id,
        '=',
        e.uuid(context!.params!.id as string)
      ),
    }))
    .run(client);
  return {props: {post: post!}};
};

export type GetPost = InferGetServerSidePropsType<typeof getServerSideProps>;

const Post: React.FC<GetPost> = (props) => {
  return (
    <div
      style={{
        margin: 'auto',
        width: '100%',
        maxWidth: '600px',
      }}
    >
      <h1 style={{padding: '50px 0px'}}>{props.post.title}</h1>
      <p style={{color: '#666'}}>{props.post.content}</p>
    </div>
  );
};

export default Post;

Inside getServerSideProps we’re extracting the id parameter from context.params and using it in our EdgeQL query. The query is a select query that fetches the id, title, and content of the post with a matching id.

We’re using Next’s InferGetServerSidePropsType utility to extract the inferred type of our query and pass it into React.FC. Now, if we update our query, the type of the component props will automatically update too. In fact, this entire application is end-to-end typesafe.

Now, click on one of the blog post links on the homepage. This should bring you to /post/<uuid>, which should display something like this:

Basic blog homepage with static content

#1 Deploy EdgeDB

First deploy an EdgeDB instance on your preferred cloud provider:

or use a cloud-agnostic deployment method:

#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.

Copy
$ 
npx edgedb migrate --dsn <your-instance-dsn> --tls-security insecure

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:

Copy
$ 
npx edgedb --dsn <your-instance-dsn> --tls-security insecure
EdgeDB 2.x (repl 2.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.

Copy
// 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 to insecure. This will disable EdgeDB’s default TLS checks; configuring TLS is beyond the scope of this tutorial.

Setting environment variables in Vercel

#6 View the application

Once deployment has completed, view the application at the deployment URL supplied by Vercel.

Admittedly this isn’t the prettiest blog of all time, or the most feature-complete. But this tutorial demonstrates how to work with EdgeDB in a Next.js app, including data fetching with API routes and getServerSideProps.

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.