Back to Blog

Zero-Dependency Core: Universal Runtime Architecture

Discover how ObjectQL's core engine achieves universal compatibility by eliminating all dependencies, enabling it to run anywhere from Node.js to browsers to edge functions.

by ObjectQL Team
architectureperformanceuniversaldeep-dive

Zero-Dependency Core: Universal Runtime Architecture

One of ObjectQL's most distinctive architectural decisions is its zero-dependency core. The @objectql/core package has exactly zero dependencies on external npm packages and zero dependencies on Node.js native modules. This enables true universal compatibility.

The Dependency Problem

Traditional Node.js ORMs are tightly coupled to the Node.js runtime:

// TypeORM's package.json (simplified)
{
  "dependencies": {
    "reflect-metadata": "^0.1.13",
    "tslib": "^2.0.0",
    "@types/node": "^16.0.0",
    // ... many more
  }
}

This creates several problems:

Problem 1: Cannot Run in Browsers

import { createConnection } from 'typeorm';
 
// ❌ Error in browser:
// Module not found: Can't resolve 'fs'
// Module not found: Can't resolve 'path'
// Module not found: Can't resolve 'crypto'

Problem 2: Cannot Run on Edge Functions

// Vercel Edge Function
export const config = { runtime: 'edge' };
 
// ❌ Error:
// Dynamic Code Evaluation not allowed in Edge Runtime

Problem 3: Large Bundle Sizes

# TypeORM bundle size
$ npm install typeorm
+ typeorm@0.3.x added 47 packages
 
# Prisma bundle size
$ npm install prisma
+ prisma@5.x added 2 packages, and audited 3 packages in 5s
# But Prisma client binary: 30+ MB

The ObjectQL Solution: Layer Separation

ObjectQL separates concerns into distinct layers with clear boundaries:

┌─────────────────────────────────────────┐
│     @objectql/types (0 dependencies)    │  Pure TypeScript interfaces
└─────────────────┬───────────────────────┘

┌─────────────────▼───────────────────────┐
│     @objectql/core (0 dependencies)     │  Universal logic engine
└─────────────────┬───────────────────────┘

        ┌─────────┼─────────┐
        │         │         │
┌───────▼──┐ ┌───▼─────┐ ┌─▼──────────┐
│  Node.js │ │ Browser │ │ Edge/Deno  │  Platform adapters
│ Platform │ │ Platform│ │  Platform  │
└──────────┘ └─────────┘ └────────────┘

Layer 1: @objectql/types

Pure TypeScript interfaces with zero dependencies:

// No imports from external packages
export interface ObjectSchema {
  name: string;
  fields: Record<string, FieldSchema>;
}
 
export interface Query {
  object: string;
  filters?: FilterClause[];
}

Bundle size: ~10 KB (just type definitions)

Layer 2: @objectql/core

The runtime engine with zero dependencies:

// @objectql/core/src/repository.ts
import type { Driver, Query } from '@objectql/types';
 
export class Repository {
  constructor(private driver: Driver) {}
  
  async find(query: Query) {
    // Pure JavaScript logic, no external dependencies
    const validated = this.validateQuery(query);
    const withPermissions = this.injectPermissions(validated);
    return await this.driver.find(withPermissions);
  }
  
  private validateQuery(query: Query): Query {
    // Pure validation logic using only:
    // - JavaScript built-ins (Object, Array, String, etc.)
    // - TypeScript interfaces from @objectql/types
    // - NO external packages
    // - NO Node.js native modules
  }
}

Bundle size: ~50 KB (minified + gzipped)

Layer 3: Platform Adapters

Platform-specific utilities are separate packages:

// @objectql/platform-node (Node.js specific)
import * as fs from 'fs';
import * as path from 'path';
 
export class FileSystemLoader {
  loadYAML(dir: string): ObjectSchema[] {
    // Node.js-specific file system operations
  }
}
 
// @objectql/platform-browser (Browser specific)
export class LocalStorageLoader {
  loadSchemas(): ObjectSchema[] {
    // Browser-specific localStorage operations
  }
}

How Zero Dependencies Works

1. Pure JavaScript Algorithms

Instead of importing libraries, @objectql/core implements algorithms in pure JavaScript:

// ❌ Don't do this (adds lodash dependency)
import _ from 'lodash';
const unique = _.uniq(array);
 
// ✅ Do this (pure JavaScript)
const unique = [...new Set(array)];
// ❌ Don't do this (adds uuid dependency)
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
 
// ✅ Do this (use driver's ID generator or pure JS)
const id = crypto.randomUUID(); // Native in modern browsers & Node.js
// Or: let driver generate IDs

2. Minimal API Surface

The core API is deliberately minimal:

// Just 5 core methods
interface Repository {
  find(query: Query): Promise<Record[]>;
  findOne(query: Query): Promise<Record | null>;
  insert(object: string, doc: Record): Promise<Record>;
  update(object: string, id: string, doc: Record): Promise<Record>;
  delete(object: string, id: string): Promise<void>;
}

No method chaining, no query builders, no complex DSLs—just pure data structures.

3. JSON-First Design

Everything is JSON-serializable:

// This query can be:
const query = {
  object: 'project',
  filters: [{ field: 'status', operator: 'eq', value: 'active' }]
};
 
// ✅ Sent over HTTP
fetch('/api/query', {
  method: 'POST',
  body: JSON.stringify(query)
});
 
// ✅ Stored in localStorage
localStorage.setItem('savedQuery', JSON.stringify(query));
 
// ✅ Passed to Web Workers
worker.postMessage({ type: 'query', data: query });
 
// ✅ Used in any JavaScript environment

4. Dependency Injection for Platform-Specific Features

Instead of hardcoding platform-specific code, use dependency injection:

// ❌ Don't do this (couples core to Node.js)
import * as fs from 'fs';
 
class Core {
  loadSchemas() {
    return fs.readFileSync('./schemas.yml', 'utf8');
  }
}
 
// ✅ Do this (inject platform-specific loader)
class Core {
  constructor(private loader: SchemaLoader) {}
  
  loadSchemas() {
    return this.loader.load(); // Loader is platform-specific
  }
}

Running Everywhere: Examples

Node.js Application

import { Repository } from '@objectql/core';
import { SQLDriver } from '@objectql/driver-sql';
import { loadSchemas } from '@objectql/platform-node';
 
// Load schemas from file system
const schemas = loadSchemas('./src/objects');
 
const driver = new SQLDriver({ /* ... */ });
const repository = new Repository({ driver, schemas });

Browser Application

import { Repository } from '@objectql/core';
import { LocalStorageDriver } from '@objectql/driver-localstorage';
 
// Schemas bundled with application
import schemas from './schemas.json';
 
const driver = new LocalStorageDriver();
const repository = new Repository({ driver, schemas });

Cloudflare Worker

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { Repository } = await import('@objectql/core');
    const { D1Driver } = await import('@objectql/driver-d1');
    
    const driver = new D1Driver({ database: env.DB });
    const repository = new Repository({ 
      driver,
      schemas: JSON.parse(env.SCHEMAS) // From environment
    });
    
    const projects = await repository.find({
      object: 'project',
      filters: []
    });
    
    return Response.json(projects);
  }
};

Deno Application

import { Repository } from 'npm:@objectql/core';
import { SQLDriver } from 'npm:@objectql/driver-sql';
 
const driver = new SQLDriver({
  client: 'postgres',
  connection: Deno.env.get('DATABASE_URL')
});
 
const repository = new Repository({ driver });

React Native App

import { Repository } from '@objectql/core';
import { SQLiteDriver } from '@objectql/driver-sqlite-rn';
import AsyncStorage from '@react-native-async-storage/async-storage';
 
const driver = new SQLiteDriver({
  name: 'myapp.db',
  location: 'default'
});
 
const repository = new Repository({ driver });

Performance Benefits

1. Faster Installation

# ObjectQL core
$ npm install @objectql/core
+ @objectql/core@1.0.0 added 1 package in 0.5s
 
# TypeORM (comparison)
$ npm install typeorm
+ typeorm@0.3.x added 47 packages in 8.2s

2. Smaller Bundle Sizes

# ObjectQL (minified + gzipped)
@objectql/types:  10 KB
@objectql/core:   50 KB
Total:            60 KB
 
# TypeORM (minified + gzipped)
typeorm:          180 KB
 
# Prisma Client (uncompressed)
@prisma/client:   30+ MB (binary)

3. Tree-Shaking Friendly

// Import only what you need
import { Repository } from '@objectql/core';
 
// Unused code is automatically removed by bundlers
// Final bundle: ~50 KB instead of full package

4. Cold Start Performance

AWS Lambda cold start comparison:

Prisma:    800ms - 1200ms (loads 30MB client binary)
TypeORM:   300ms - 500ms  (loads reflect-metadata, many deps)
ObjectQL:  50ms - 100ms   (pure JavaScript, minimal code)

5. Memory Footprint

Prisma:    ~50 MB  (query engine + Node.js runtime)
TypeORM:   ~30 MB  (many dependencies)
ObjectQL:  ~10 MB  (core + single driver)

Universal Compatibility Matrix

Environment@objectql/core@objectql/driver-sql@objectql/driver-memory
Node.js
Browser
Deno
Cloudflare Workers
Vercel Edge
React Native
Electron

The core runs everywhere. Only platform-specific drivers have limitations.

Real-World Use Cases

Use Case 1: Offline-First Mobile App

// Mobile app with sync capability
import { Repository } from '@objectql/core';
import { SQLiteDriver } from '@objectql/driver-sqlite-rn';
import { SyncDriver } from '@objectql/driver-sync';
 
// Local storage
const localDriver = new SQLiteDriver({ name: 'app.db' });
const localRepo = new Repository({ driver: localDriver });
 
// Remote API (when online)
const remoteDriver = new SyncDriver({ 
  url: 'https://api.example.com',
  token: userToken
});
const remoteRepo = new Repository({ driver: remoteDriver });
 
// App code works with both identically
async function saveTask(task: Task) {
  // Save locally
  await localRepo.insert('task', task);
  
  // Sync to server when online
  if (navigator.onLine) {
    await remoteRepo.insert('task', task);
  }
}

Use Case 2: Edge-Rendered Dashboard

// Vercel Edge Function
import { Repository } from '@objectql/core';
import { MemoryDriver } from '@objectql/driver-memory';
 
export const config = { runtime: 'edge' };
 
// Pre-seed data at build time
const driver = new MemoryDriver();
await driver.insert('stat', { name: 'users', count: 1000 });
 
const repository = new Repository({ driver });
 
export default async function handler(req: Request) {
  const stats = await repository.find({ object: 'stat' });
  
  return new Response(
    JSON.stringify(stats),
    { headers: { 'Content-Type': 'application/json' } }
  );
}

Use Case 3: Browser-Based Database

// Complete database in the browser
import { Repository } from '@objectql/core';
import { LocalStorageDriver } from '@objectql/driver-localstorage';
 
const driver = new LocalStorageDriver({ prefix: 'myapp_' });
const repository = new Repository({ driver });
 
// Works offline, persists across sessions
await repository.insert('note', {
  title: 'Meeting Notes',
  content: 'Discussed ObjectQL architecture...'
});
 
// Reload page - data still there
const notes = await repository.find({ object: 'note' });

Implementation Insights

How Core Avoids Dependencies

1. Schema Validation

// Instead of: import Ajv from 'ajv'; (adds dependency)
// Implement simple validation in pure JavaScript
 
function validateSchema(schema: ObjectSchema): void {
  if (!schema.name || typeof schema.name !== 'string') {
    throw new ValidationError('Schema must have a name');
  }
  
  if (!schema.fields || typeof schema.fields !== 'object') {
    throw new ValidationError('Schema must have fields');
  }
  
  for (const [fieldName, fieldSchema] of Object.entries(schema.fields)) {
    validateFieldSchema(fieldName, fieldSchema);
  }
}

2. Query Parsing

// Instead of: import { parse } from 'query-string'; (adds dependency)
// Use native URLSearchParams
 
function parseQueryParams(url: string): Record<string, any> {
  const params = new URLSearchParams(new URL(url).search);
  return Object.fromEntries(params.entries());
}

3. Deep Cloning

// Instead of: import cloneDeep from 'lodash/cloneDeep';
// Use native structuredClone (available in modern JS)
 
function deepClone<T>(obj: T): T {
  return structuredClone(obj);
}

4. Date Formatting

// Instead of: import dayjs from 'dayjs';
// Use native Intl.DateTimeFormat
 
function formatDate(date: Date, format: string): string {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit'
  }).format(date);
}

Trade-offs and Considerations

Advantages

Universal compatibility - runs anywhere JavaScript runs ✅ Tiny bundle size - 50 KB vs 180+ KB for alternatives ✅ Fast cold starts - critical for serverless ✅ No dependency hell - no conflicting versions ✅ Future-proof - no risk of abandoned dependencies

Potential Limitations

⚠️ Must implement features in-house - can't rely on external libraries ⚠️ Platform features require adapters - file system, crypto, etc. ⚠️ May reinvent some wheels - validation, parsing, etc.

However, these trade-offs are worthwhile for the benefits of universal compatibility.

Best Practices

1. Keep Core Pure

// ✅ Good - pure function
export function mergeQueries(q1: Query, q2: Query): Query {
  return {
    ...q1,
    filters: [...(q1.filters || []), ...(q2.filters || [])]
  };
}
 
// ❌ Bad - Node.js specific
export function loadQueryFromFile(path: string): Query {
  return JSON.parse(fs.readFileSync(path, 'utf8'));
}

2. Use Platform Adapters for I/O

// ✅ Core package
export interface SchemaLoader {
  load(): Promise<ObjectSchema[]>;
}
 
// ✅ Platform-specific package
// @objectql/platform-node
export class FileSystemLoader implements SchemaLoader {
  async load() {
    const files = fs.readdirSync(this.dir);
    return files.map(f => loadYAML(f));
  }
}

3. Provide Sensible Defaults

// Core provides default implementations using standard APIs
export function generateId(): string {
  // Uses native crypto API (available everywhere now)
  return crypto.randomUUID();
}
 
// But allow injection for special cases
export class Repository {
  constructor(
    private driver: Driver,
    private idGenerator: () => string = generateId
  ) {}
}

Conclusion

ObjectQL's zero-dependency architecture enables true "write once, run anywhere" capability:

  • 🌐 Universal: One codebase for server, browser, edge, and mobile
  • Performant: Minimal bundle size, fast cold starts, efficient memory usage
  • 🔮 Future-proof: No external dependencies to maintain or update
  • 🎯 Focused: Core solves one problem well—data access abstraction

By carefully separating universal logic from platform-specific features, ObjectQL achieves something rare in the Node.js ecosystem: true portability.

The next time you write a library, ask yourself: "Does this really need that dependency, or can I use native JavaScript?"

Learn More


Series Complete! Check out all articles in the Architecture Deep Dive series.