Introduction
EdgeDB revolutionizes databases in 2026 by blending PostgreSQL's relational power with native graph semantics via EdgeQL, a declarative, type-safe language. For expert developers, it's the ideal tool for modeling complex domains like social networks or e-commerce, where traditional SQL N-N joins explode in complexity. Imagine querying a users → posts → comments graph in one line instead of 5 JOINs: 10x perf gains, auto-migrated schemas, and native JS clients. This tutorial walks you through a concrete blog app example: installation, advanced schema with backlinks, optimized CRUD, Next.js integration, and scaling pitfalls. By the end, you'll deploy a production-ready instance in <30min, ready for 1M+ nodes.
Prerequisites
- Docker 24+ (for isolated instances)
- Node.js 20+ and npm/yarn
- Advanced knowledge of TypeScript, graphs, and SQL
- Git to clone an example repo (optional)
- 4GB free RAM for local tests
Install EdgeDB CLI and Start an Instance
curl --proto '=https' --tlsv1.2 -sSf https://sh edgedb.com | sh
source ~/.bashrc
edgedb instance create devblog \
--dsn postgres://edgedb@localhost:5432/postgres \
--password edgedb \
--if-not-exists
edgedb instance start devblog
edgedb -I devblog instance statusThis script downloads the EdgeDB CLI (2026 stable version), creates a 'devblog' instance on an integrated Postgres container, and starts it. Use --dsn to point to an external Postgres in production; --if-not-exists avoids duplicates. Check with status: wait for 'Ready to accept connections' to confirm.
Understanding Instances and Connections
An EdgeDB instance encapsulates a Postgres cluster with a graph engine. Connect via edgedb CLI or DSN edgedb://username@localhost:10711/devblog. For production, scale with Kubernetes: expose port 10711 and use secrets for passwords. Test the connection: edgedb -I devblog query 'SELECT 1;' should return [{1}].
Define the Base Schema (User and Post)
module default {
type User {
required name: str;
required email: str {
constraint exclusive;
};
posts: array of Post;
}
type Post {
required title: str;
required content: str;
required owner: User;
viewers: array of User;
}
scalar type status_t extending str {
constraint one_of('draft', 'published');
};
}This .esdl module defines User with posts (array for 1-N) and Post with owner (required single link) and viewers (N-N multi). scalar type extends str with constraints for status. Save as dbschema/default.esdl, apply via edgedb migration create then edgedb migrate. Avoid pitfalls: always use required for immutables, exclusive on emails.
Apply the Schema and First Migration
Copy schema.esdl to dbschema/default.esdl. Run edgedb migration create --from-latest: EdgeDB auto-generates a reversible migration. Apply with edgedb migrate. Verify: edgedb describe modules lists types. For iterations, EdgeDB tracks diffs and suggests smart merges.
Basic Insert and Query (CRUD User/Post)
INSERT User { name := 'Alice', email := 'alice@ex.com' };
INSERT Post { title := 'Mon premier post', content := 'Contenu...', owner := (SELECT User FILTER .email = 'alice@ex.com' LIMIT 1) };
SELECT User { name, posts: { title } } FILTER .name = 'Alice';
UPDATE Post FILTER .title = 'Mon premier post' SET { content += ' Ajouté!' };
DELETE Post FILTER .title LIKE '%premier%';These EdgeQL queries insert User/Post with links via subquery FILTER (safer than ID). SELECT projects nested (posts.title), UPDATE appends (+=), DELETE filters LIKE. Run block-by-block in edgedb. Pitfall: LIMIT 1 on single links avoids ambiguities; use @@ for computed.
Master Advanced Relationships (Comment + Backlinks)
Add Comment with multi-links. Backlinks auto-expose inverses (.owner <- post). For N-N viewers: ALTER TYPE Post { multi link viewers := {} ; }. Graph query: SELECT User { posts: { viewers: { name } } } unfolds everything in one request.
Extended Schema with Comment and Backlinks
using default;
abstract type Content {
required created_at: datetime { readonly := true };
};
type Comment extending Content {
required text: str;
required author: User;
required post: Post;
};
ALTER TYPE Post {
comments: array of Comment;
};
ALTER TYPE User {
authored_comments: array of Comment;
};Extends with abstract Content for readonly timestamps (auto-filled). Adds Comment linked to Post/User. Backlinks auto-generated on Post.comments and User.authored_comments. Migrate: edgedb migration create. Benefit: queries like SELECT Post { comments: { author: { name } } } without JOINs.
Expert Query: Full Graph with Aggregates
WITH recent_posts := (SELECT Post ORDER BY .created_at DESC LIMIT 10)
SELECT User {
name,
post_count := count(.posts),
avg_post_len := math::mean(.posts.content) ?? 0.0,
recent_posts: { title, comments: { text, author: { name } } ORDER BY .created_at }
} FILTER exists .posts
ORDER BY .post_count DESC
LIMIT 5;WITH for reusable CTE, projects computed fields (count, math::mean), filters exists (non-null), multi-level ordering. ?? 0.0 handles null fallback. Run after inserts: returns top users by recent posts with nested comments. Perf: auto-index on created_at.
Integrate with TypeScript Client (Next.js)
Install @edgedb/js: npm i @edgedb/js. Create a pooled client for scaling. In Next.js, use it in API routes or RSC.
Generated TypeScript Client and Usage
import { createClient } from 'edgedb';
import e from './dbschema/edgeql-js'; // généré via edgedb gen
const client = createClient();
export async function getTopUsers() {
return e.select(e.User, (user) => ({ // type-safe!
filter_single: { name: 'Alice' },
name: true,
posts: { title: true },
}));
}
export async function createPost(userId: string, data: {title: string, content: string}) {
return e.insert(e.Post, {
title: data.title,
content: data.content,
owner: e.select(e.User, (u) => ({ filter_single: { id: userId } })),
});
}
await client.close();Generate types via npx edgedb-js codegen from schema. Pooled client auto-manages connections. Type-safe queries: IDE autocompletes filter_single. Use async/await; close() on shutdown. Pitfall: always await client.ensureConnected().
Best Practices
- Always generate JS code:
edgedb-js codegenfor type-safe, refactor-proof queries. - Proactively index:
ALTER TYPE Post CREATE INDEX ON (.created_at)for hot paths. - Use global edges for computed:
global current_user: User;in auth. - Migrate in CI/CD:
edgedb migrate --dev-modethenedgedb migrate --to-revision HEAD. - Monitor with
edgedb instance logsand built-in Prometheus exporter.
Common Errors to Avoid
- Forgetting atomicity: EdgeDB is ACID, but use
FOR ... UNIONfor batch inserts perf, not loops. - Unindexed queries:
EXPLAINreveals scans; addCREATE INDEX ON (User FILTER .email). - Mismanaged cardinality:
single linkvsmulti: runtime error if >1; use?for optional. - Manual migrations: Never
RUN MIGRATIONwithout review; trust auto-gen but validate diffs.
Next Steps
- Official docs: EdgeDB.com
- Full example repo: GitHub EdgeDB blog
- Advanced video: Distributed graph queries
- Learni trainings on graph DBs: master Neo4j + EdgeDB in bootcamp.
- EdgeDB Discord community for real-world patterns.