Skip to content
Learni
View all tutorials
Bases de données

How to Master EdgeDB for Expert Apps in 2026

Lire en français

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

terminal-install.sh
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 status

This 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)

schema.esdl
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)

query-crud.edgeql
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

schema-extended.esdl
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

query-advanced.edgeql
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

lib/edgedb.ts
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 codegen for 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-mode then edgedb migrate --to-revision HEAD.
  • Monitor with edgedb instance logs and built-in Prometheus exporter.

Common Errors to Avoid

  • Forgetting atomicity: EdgeDB is ACID, but use FOR ... UNION for batch inserts perf, not loops.
  • Unindexed queries: EXPLAIN reveals scans; add CREATE INDEX ON (User FILTER .email).
  • Mismanaged cardinality: single link vs multi: runtime error if >1; use ? for optional.
  • Manual migrations: Never RUN MIGRATION without review; trust auto-gen but validate diffs.

Next Steps