GraphQL, built for Deno.
Obsidian is a high-performance GraphQL client and server library for Deno, featuring intelligent caching, query normalization, and first-class GitHub API support. Whether you're building a GraphQL client for any API or specifically need a Deno-native GitHub client, Obsidian provides the tools you need.
- 🚀 Deno-native: Built from the ground up for Deno
- ⚡ Intelligent caching: Multiple cache algorithms (LFU, LRU, W-TinyLFU) with automatic normalization
- 🔒 Production-ready: DoS protection, persistent queries, and comprehensive error handling
- 🐙 GitHub-first: Specialized
GithubClientwith rate limit tracking and GitHub API optimizations - 📦 JSR package: Published on JSR for Deno and for Node.js/Bun client usage via JSR's npm compatibility layer
If you're building a GitHub integration or automation tool, GithubClient
provides a streamlined experience:
import { GithubClient } from "jsr:@chesapeake/obsidian-gql";
const github = new GithubClient({
token: Deno.env.get("GITHUB_TOKEN"),
useCache: true,
});
// Query a repository
const response = await github.query(
`query GetRepo($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
description
stargazerCount
forkCount
}
}`,
{ variables: { owner: "denoland", name: "deno" } },
);
// Check rate limits
const rateLimit = github.getRateLimit();
console.log(`Remaining: ${rateLimit?.remaining}/${rateLimit?.limit}`);Key features for GitHub users:
- Automatic POST-only requests (GitHub requirement)
- Built-in rate limit tracking
- Token convenience (automatically sets Authorization header)
- Default endpoint configured for GitHub API
If you need a GraphQL client for any API, ObsidianClient provides flexible
caching and powerful features:
import { ObsidianClient } from "jsr:@chesapeake/obsidian-gql";
const client = new ObsidianClient({
endpoint: "https://api.example.com/graphql",
useCache: true,
algo: "LRU", // "LFU" | "LRU" | "W-TinyLFU"
capacity: 5000,
headers: {
Authorization: "Bearer your-token",
},
});
// Query with variables
const response = await client.query(
`query GetUser($id: ID!) {
user(id: $id) {
name
email
}
}`,
{ variables: { id: "123" } },
);
// Mutation
const mutation = await client.mutate(
`mutation UpdateUser($id: ID!, $name: String!) {
updateUser(id: $id, name: $name) {
id
name
}
}`,
{ variables: { id: "123", name: "New Name" } },
);
// Always check both data and errors
if (response.errors) {
console.error("GraphQL errors:", response.errors);
}
if (response.data) {
console.log("Data:", response.data);
}Install from JSR:
import {
GithubClient, // GitHub-specific client
gql, // GraphQL tag helper
ObsidianClient, // General GraphQL client
ObsidianService, // GraphQL server handler
} from "jsr:@chesapeake/obsidian-gql";No configuration needed - Deno will automatically download and cache the package on first import.
| Component | Deno | Node.js / Bun |
|---|---|---|
ObsidianClient, GithubClient, gql |
Supported | Supported |
ObsidianService (server) |
Supported | Not yet supported |
Node.js and Bun compatibility currently applies to the client API only.
ObsidianService depends on Deno-specific server dependencies (for example
@akin01/deno-redis) and is intended for Deno deployments today. Server-side
Node.js support is tracked in
GitHub issue #1.
- Multiple cache algorithms: Choose from LFU, LRU, or W-TinyLFU based on your access patterns
- Query normalization: Efficient cache storage that enables cache hits even for different query shapes
- Persistent queries: Minimize network payload by sending query hashes instead of full queries
- Smart cache invalidation: Automatic cache clearing on mutations
- Authentication helpers:
beforeFetchcallback for dynamic headers,onAuthErrorfor auth error handling - TypeScript support: Full type definitions included
- Redis-backed caching: Shared server-side cache for high-performance applications
- Query depth limiting: Built-in DoS protection
- Query normalization: Server-side normalization for efficient cache storage
- Persistent queries: Hash-based query storage
import { ObsidianClient } from "jsr:@chesapeake/obsidian-gql";
const client = new ObsidianClient({
endpoint: "/graphql",
useCache: true,
algo: "LRU", // "LFU" | "LRU" | "W-TinyLFU"
capacity: 5000,
persistQueries: true,
searchTerms: ["title", "director", "genre"], // For cache normalization
headers: {
Authorization: "Bearer your-token",
},
// Optional: Modify requests before fetch (e.g., refresh tokens)
beforeFetch: (request) => {
const headers = new Headers(request.options.headers as HeadersInit);
headers.set("Authorization", `Bearer ${getToken()}`);
return { ...request.options, headers: Object.fromEntries(headers) };
},
// Optional: Handle authentication errors
onAuthError: (error) => {
console.error("Auth error:", error.status, error.graphqlErrors);
// Handle token refresh, redirect to login, etc.
},
});
// Query without variables
const response = await client.query(`query {
movies {
id
title
releaseYear
}
}`);
// Query with variables (recommended)
const repoResponse = await client.query(
`query GetRepo($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
description
}
}`,
{ variables: { owner: "denoland", name: "deno" } },
);
// Mutation with variables
const mutation = await client.mutate(
`mutation AddMovie($title: String!, $year: Int!) {
addMovie(input: {title: $title, releaseYear: $year}) {
id
title
}
}`,
{ variables: { title: "Movie", year: 2024 } },
);Important Notes:
- Request Method: Defaults to POST for all requests. GET is supported for
queries via the
methodoption, but POST is recommended and required for mutations. - Response Shape: All responses follow the GraphQL spec format:
{ data?, errors? }. Always check both properties as partial results/errors are possible. - Variables: Use the
variablesoption to pass parameterized values. Variables are included in cache keys to prevent collisions. - Authentication: Use
beforeFetchto dynamically add auth headers, or set static headers in the constructor. UseonAuthErrorto handle 401/403 responses or GraphQL auth errors.
GithubClient extends ObsidianClient with GitHub-specific optimizations:
import { GithubClient } from "jsr:@chesapeake/obsidian-gql";
const github = new GithubClient({
token: process.env.GITHUB_TOKEN, // Automatically sets Authorization header
useCache: true,
algo: "LRU",
capacity: 5000,
});
// Query with variables
const response = await github.query(
`query GetRepo($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
name
stargazerCount
description
issues(first: 10) {
nodes {
title
author {
login
}
}
}
}
}`,
{ variables: { owner: "denoland", name: "deno" } },
);
// Check rate limits
const rateLimit = github.getRateLimit();
if (rateLimit) {
console.log(`Rate limit: ${rateLimit.remaining}/${rateLimit.limit}`);
console.log(`Resets in: ${github.getSecondsUntilReset()} seconds`);
if (github.isRateLimited()) {
console.warn("Rate limit exhausted!");
}
}GithubClient Features:
- POST-only enforcement: All requests use POST (GitHub requirement)
- Default endpoint: Automatically uses
https://api.github.com/graphql - Token convenience: Pass
tokenoption to automatically setAuthorization: Bearerheader - Rate limit tracking: Automatically captures rate limit headers
(
X-RateLimit-*) - GitHub-specific headers: Sets appropriate
Acceptheader for GitHub API
Build a GraphQL server with Redis-backed caching:
import { gql, ObsidianService } from "jsr:@chesapeake/obsidian-gql";
import { resolvers } from "./resolvers.ts";
import { types } from "./schema/types.ts";
const handler = await ObsidianService({
typeDefs: types,
resolvers: resolvers,
useCache: true,
redisPort: 6379,
persistQueries: true,
maxQueryDepth: 10, // DoS protection
});
Deno.serve({ port: 8000 }, handler);Server Configuration:
- Redis: Required for server-side caching. Set
REDIS_HOSTenvironment variable (default:127.0.0.1) - Redis Port: Configure via
redisPortoption (default:6379) - Query Depth: Set
maxQueryDepthto prevent DoS attacks - Persistent Queries: Enable
persistQueriesto use query hashes instead of full queries
For server-side caching with ObsidianService, Redis must be running. Create a
.env file:
REDIS_HOST=127.0.0.1Configure the Redis port via ObsidianService options (default: 6379):
const handler = await ObsidianService({
typeDefs: types,
resolvers: resolvers,
redisPort: 6379, // Optional, defaults to 6379
});Obsidian supports three cache algorithms, each optimized for different access patterns:
- LFU (Least Frequently Used): Best for workloads where frequently accessed items should stay cached
- LRU (Least Recently Used): Best for workloads with temporal locality (recent items are likely to be accessed again)
- W-TinyLFU: Advanced algorithm combining frequency and recency, ideal for mixed workloads
Choose based on your application's access patterns. W-TinyLFU is recommended for most use cases.
See the examples/ directory for complete working examples.
- Deno: Version 1.28.0 or higher
- Redis: Required only for server-side caching (optional for client-only usage)
Full documentation available at getobsidian.io
We welcome contributions! See CONTRIBUTING.md for development guidelines and setup instructions.
See LICENSE for details.
David Kim
David Norman
Eileen Cho
Joan Manto
Alex Lopez
Kevin Huang
Matthew Weisker
Ryan Ranjbaran
Derek Okuno
Liam Johnson
Josh Reed
Jonathan Fangon
Liam Jeon
Yurii Shchyrba
Linda Zhao
Ali Fay
Anthony Guan
Yasir Choudhury
Yogi Paturu
Michael Chin
Dana Flury
Sardor Akhmedov
Christopher Berry
Olivia Yeghiazarian
Michael Melville
John Wong
Kyung Lee
Justin McKay
Patrick Sullivan
Cameron Simmons
Raymond Ahn
Alonso Garza
Burak Caliskan
Matt Meigs
Travis Frank
Lourent Flores
Esma Sahraoui
Derek Miller
Eric Marcatoma
Spencer Stockton