Skip to content

Boilerplate Sucks 👎🏽

We're re-aligning our packages into a new streamlined installation and setup experience.
Below you'll find the current boilerplate heavy setup.

Curious? Read the RFC

Setup

All frameworks should follow this configuration first.

Configure the Build Plugin

WarpDrive uses a babel plugin to inject app-specific configuration allowing us to provide advanced dev-mode debugging features, deprecation management, and canary feature toggles.

For Ember.js, this plugin comes built-in to the toolchain and all you need to do is provide it the desired configuration in ember-cli-build. For all other projects, the configuration is done inside of the app's babel configuration file.

ts
'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const { compatBuild } = require('@embroider/compat');

module.exports = async function (defaults) {
  const { setConfig } = await import('@warp-drive/build-config'); 
  const { buildOnce } = await import('@embroider/vite');
  const app = new EmberApp(defaults, {});

  setConfig(app, __dirname, { 
    // this should be the most recent <major>.<minor> version for
    // which all deprecations have been fully resolved
    // and should be updated when that changes
    // for new apps it should be the version you installed
    compatWith: '5.5'
  });

  return compatBuild(app, buildOnce);
};
ts
'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const { compatBuild } = require('@embroider/compat');

module.exports = async function (defaults) {
  const { setConfig } = await import('@warp-drive/build-config'); 
  const { buildOnce } = await import('@embroider/vite');
  const app = new EmberApp(defaults, {});

  setConfig(app, __dirname, { 
    // this should be the most recent <major>.<minor> version for
    // which all deprecations have been fully resolved
    // and should be updated when that changes
    compatWith: '4.12'
    deprecations: {
      // ... list individual deprecations that have been resolved here
    }
  });

  return compatBuild(app, buildOnce);
};
ts
import { setConfig } from '@warp-drive/build-config';

setConfig(context, {
  // this should be the most recent <major>.<minor> version for
  // which all deprecations have been fully resolved
  // and should be updated when that changes
  // for new apps it should be the version you installed
  // for universal apps this MUST be at least 5.5
  compatWith: '5.5'
});

Add TypeScript Types

tsconfig.json
{
  compilerOptions: {
    types: [
      "@ember-data/graph/unstable-preview-types",
      "@ember-data/json-api/unstable-preview-types",
      "@ember-data/request/unstable-preview-types",
      "@ember-data/request-utils/unstable-preview-types",
      "@ember-data/store/unstable-preview-types",
      "@warp-drive/build-config/unstable-preview-types",
      "@warp-drive/core-types/unstable-preview-types",
      "@warp-drive/schema-record/unstable-preview-types",
    ]
  }
}
tsconfig.json
{
  compilerOptions: {
    types: [
      "@ember-data/debug/unstable-preview-types",
      "@ember-data/graph/unstable-preview-types",
      "@ember-data/json-api/unstable-preview-types",
      "@ember-data/request/unstable-preview-types",
      "@ember-data/request-utils/unstable-preview-types",
      "@ember-data/store/unstable-preview-types",
      "@warp-drive/build-config/unstable-preview-types",
      "@warp-drive/core-types/unstable-preview-types",
      "@warp-drive/ember/unstable-preview-types",
      "@warp-drive/schema-record/unstable-preview-types",
    ]
  }
}
tsconfig.json
{
  compilerOptions: {
    types: [
      "@ember-data/adapter/unstable-preview-types",
      "@ember-data/debug/unstable-preview-types",
      "@ember-data/graph/unstable-preview-types",
      "@ember-data/json-api/unstable-preview-types",
      "@ember-data/legacy-compat/unstable-preview-types",
      "@ember-data/model/unstable-preview-types",
      "@ember-data/request/unstable-preview-types",
      "@ember-data/request-utils/unstable-preview-types",
      "@ember-data/serializer/unstable-preview-types",
      "@ember-data/store/unstable-preview-types",
      "@warp-drive/build-config/unstable-preview-types",
      "@warp-drive/core-types/unstable-preview-types",
      "@warp-drive/ember/unstable-preview-types",
      "@warp-drive/schema-record/unstable-preview-types",
    ]
  }
}

Configure the Store

To get up and running we need to configure a Store to understand how we want to handle requests, what our data looks like, how to cache it, and what sort of reactive objects to create for that data.

Here's an example final configuration. Below we'll show each bit in parts and discuss what each does.

💡 Guide

Looking for Legacy Adapter/Serializer Support?

→ After finishing this page read the guide for Ember.js

ts
import Store, { CacheHandler } from '@ember-data/store';
import type { CacheCapabilitiesManager } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import { CachePolicy } from '@ember-data/request-utils';

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types';
import {
  instantiateRecord,
  registerDerivations,
  SchemaService,
  teardownRecord
} from '@warp-drive/schema-record';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  lifetimes = new CachePolicy({
    apiHardExpires: 15 * 60 * 1000, // 15 minutes
    apiSoftExpires: 1 * 30 * 1000, // 30 seconds
    constraints: {
      'X-WarpDrive-Expires': true,
      'Cache-Control': true,
      'Expires': true,
    }
  });

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createArgs?: Record<string, unknown>) {
    return instantiateRecord(this, identifier, createArgs);
  }

  teardownRecord(record: unknown): void {
    return teardownRecord(record);
  }
}
ts
import Store, { CacheHandler } from '@ember-data/store';
import type { CacheCapabilitiesManager, ModelSchema, SchemaService } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import { CachePolicy } from '@ember-data/request-utils';

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types';
import type { TypeFromInstance } from '@warp-drive/core-types/record';

import type Model from '@ember-data/model';
import {
  buildSchema,
  instantiateRecord,
  modelFor,
  teardownRecord
} from '@ember-data/model/hooks';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  lifetimes = new CachePolicy({
    apiHardExpires: 15 * 60 * 1000, // 15 minutes
    apiSoftExpires: 1 * 30 * 1000, // 30 seconds
    constraints: {
      'X-WarpDrive-Expires': true,
      'Cache-Control': true,
      'Expires': true,
    }
  });

  createSchemaService(): SchemaService {
    return buildSchema(this);
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createRecordArgs: Record<string, unknown>) {
    return instantiateRecord.call(this, identifier, createRecordArgs);
  }

  teardownRecord(record: unknown): void {
    return teardownRecord.call(this, record as Model);
  }

  modelFor<T>(type: TypeFromInstance<T>): ModelSchema<T>;
  modelFor(type: string): ModelSchema;
  modelFor(type: string): ModelSchema {
    return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type);
  }
}
ts
import Store, { CacheHandler, recordIdentifierFor } from '@ember-data/store';
import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import { CachePolicy } from '@ember-data/request-utils';

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types';
import type { TypeFromInstance } from '@warp-drive/core-types/record';
import { DelegatingSchemaService } from '@ember-data/model/migration-support';

import type Model from '@ember-data/model';
import {
  instantiateRecord as instantiateModel,
  modelFor,
  teardownRecord as teardownModel
} from '@ember-data/model/hooks';
import {
  instantiateRecord,
  registerDerivations,
  SchemaService,
  teardownRecord
} from '@warp-drive/schema-record';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  lifetimes = new CachePolicy({
    apiHardExpires: 15 * 60 * 1000, // 15 minutes
    apiSoftExpires: 1 * 30 * 1000, // 30 seconds
    constraints: {
      'X-WarpDrive-Expires': true,
      'Cache-Control': true,
      'Expires': true,
    }
  });

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return new DelegatingSchemaService(this, schema);
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createArgs?: Record<string, unknown>) {
    if (this.schema.isDelegated(identifier)) {
      return instantiateModel.call(this, identifier, createRecordArgs)
    }
    return instantiateRecord(this, identifier, createArgs);
  }

  teardownRecord(record: unknown): void {
    const identifier = recordIdentifierFor(record);
    if (this.schema.isDelegated(identifier)) {
      return teardownModel.call(this, record as Model);
    }
    return teardownRecord(record);
  }

  modelFor<T>(type: TypeFromInstance<T>): ModelSchema<T>;
  modelFor(type: string): ModelSchema;
  modelFor(type: string): ModelSchema {
    return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type);
  }
}

Start With A Store

The store is the central piece of the WarpDrive experience. It functions as a coordinator, linking together requests for data with schemas, caching and reactivity.

While it's easy to use just WarpDrive's request management, most apps will find they require far more than basic fetch management. For this reason it's often best to start with a Store even when you aren't sure yet.

ts
import Store from '@ember-data/store';

export default class AppStore extends Store {}

Add Basic Request Management

RequestManager provides a chain-of-responsibility style pipeline for helping you handle centralized concerns around requesting and updating data from your backend.

💡 Guide

→ Learn more about Making Requests

ts
import Store from '@ember-data/store';

import RequestManager from '@ember-data/request'; 
import Fetch from '@ember-data/request/fetch';

export default class AppStore extends Store {
  requestManager = new RequestManager() 
    .use([Fetch]);
}

Add a Source for Schema for your Data

WarpDrive uses simple JSON schemas to define the shape and features of reactive objects. Schemas may seem simple, but they come packed with features that will help you build incredible applications.

💡 Guide

→ Learn more about Resource Schemas

ts
import Store from '@ember-data/store';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import {  
  registerDerivations,
  SchemaService,
} from '@warp-drive/schema-record';

export default class AppStore extends Store {
  requestManager = new RequestManager()
    .use([Fetch]);

  createSchemaService() { 
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

}
ts
import Store from '@ember-data/store';
import type { ModelSchema, SchemaService } from '@ember-data/store/types'; 

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import type { TypeFromInstance } from '@warp-drive/core-types/record'; 

import {  
  buildSchema,
  modelFor,
} from '@warp-drive/schema-record';

export default class AppStore extends Store {
  requestManager = new RequestManager()
    .use([Fetch]);

  createSchemaService(): SchemaService { 
    return buildSchema(this);
  }

  modelFor<T>(type: TypeFromInstance<T>): ModelSchema<T>; 
  modelFor(type: string): ModelSchema;
  modelFor(type: string): ModelSchema {
    return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type);
  }
}
ts
import Store from '@ember-data/store';
import type { ModelSchema } from '@ember-data/store/types'; 

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import type { TypeFromInstance } from '@warp-drive/core-types/record'; 
import { DelegatingSchemaService } from '@ember-data/model/migration-support';

import {  
  modelFor,
} from '@warp-drive/schema-record';
import { 
  registerDerivations,
  SchemaService,
} from '@warp-drive/schema-record';

export default class AppStore extends Store {
  requestManager = new RequestManager()
    .use([Fetch]);

  createSchemaService() { 
    const schema = new SchemaService();
    registerDerivations(schema);
    return new DelegatingSchemaService(this, schema);
  }

  modelFor<T>(type: TypeFromInstance<T>): ModelSchema<T>; 
  modelFor(type: string): ModelSchema;
  modelFor(type: string): ModelSchema {
    return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type);
  }
}

Add a Cache

Do you really need a cache? Are sunsets beautiful? Caching is what powers features like immutability, mutation management, and allows WarpDrive to understand your relational data.

Some caches are simple request/response maps. WarpDrive's is not. The Cache deeply understands the structure of your data, ensuring your data remains consistent both within and across requests.

Out of the box, WarpDrive provides a Cache that expects the {JSON:API} format. This format excels at simiplifying common complex problems around cache consistency and information density. Most APIs can be quickly adapted to work with it, but if a cache built to understand another format would do better it just needs to follow the same interface.

ts
import Store, { CacheHandler } from '@ember-data/store'; 
import type { CacheCapabilitiesManager } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import JSONAPICache from '@ember-data/json-api'; 

import {
  registerDerivations,
  SchemaService,
} from '@warp-drive/schema-record';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) { 
    return new JSONAPICache(capabilities);
  }
}

Setup Your Data to be Reactive

While it is possible to use WarpDrive to store and retrieve raw json, you'd be missing out on the best part. Reactive objects transform raw cached data into rich, reactive data. The resulting objects are immutable, always displaying the latest state in the cache while preventing accidental or unsafe mutation in your app.

ts
import Store, { CacheHandler } from '@ember-data/store';
import type { CacheCapabilitiesManager } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types'; 
import {
  instantiateRecord, 
  registerDerivations,
  SchemaService,
  teardownRecord 
} from '@warp-drive/schema-record';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createArgs?: Record<string, unknown>) { 
    return instantiateRecord(this, identifier, createArgs);
  }

  teardownRecord(record: unknown): void { 
    return teardownRecord(record);
  }
}
ts
import Store, { CacheHandler } from '@ember-data/store';
import type { CacheCapabilitiesManager, ModelSchema, SchemaService } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types'; 
import type { TypeFromInstance } from '@warp-drive/core-types/record';

import type Model from '@ember-data/model'; 
import {
  buildSchema,
  instantiateRecord, 
  modelFor,
  teardownRecord  
} from '@ember-data/model/hooks';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  createSchemaService(): SchemaService {
    return buildSchema(this);
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createRecordArgs: Record<string, unknown>) {  
    return instantiateRecord.call(this, identifier, createRecordArgs);
  }

  teardownRecord(record: unknown): void {  
    return teardownRecord.call(this, record as Model);
  }

  modelFor<T>(type: TypeFromInstance<T>): ModelSchema<T>;
  modelFor(type: string): ModelSchema;
  modelFor(type: string): ModelSchema {
    return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type);
  }
}
ts
import Store, { CacheHandler, recordIdentifierFor } from '@ember-data/store';
import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types'; 
import type { TypeFromInstance } from '@warp-drive/core-types/record';
import { DelegatingSchemaService } from '@ember-data/model/migration-support';

import type Model from '@ember-data/model'; 
import {
  instantiateRecord as instantiateModel, 
  modelFor,
  teardownRecord as teardownModel 
} from '@ember-data/model/hooks';
import {
  instantiateRecord, 
  registerDerivations,
  SchemaService,
  teardownRecord 
} from '@warp-drive/schema-record';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return new DelegatingSchemaService(this, schema);
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createArgs?: Record<string, unknown>) {  
    if (this.schema.isDelegated(identifier)) {
      return instantiateModel.call(this, identifier, createRecordArgs)
    }
    return instantiateRecord(this, identifier, createArgs);
  }

  teardownRecord(record: unknown): void {  
    const identifier = recordIdentifierFor(record);
    if (this.schema.isDelegated(identifier)) {
      return teardownModel.call(this, record as Model);
    }
    return teardownRecord(record);
  }

  modelFor<T>(type: TypeFromInstance<T>): ModelSchema<T>;
  modelFor(type: string): ModelSchema;
  modelFor(type: string): ModelSchema {
    return (modelFor.call(this, type) as ModelSchema) || super.modelFor(type);
  }
}

Decide How Long Requests are Valid for with a CachePolicy

And of course, what's a great cache without an eviction policy?

WarpDrive provides an interface for creating Cache Policies. Whenever a request is made, the policy is checked to determine if the current cached representation is still valid.

Policies also have the ability to subscribe to cache updates and issue invalidation notifications. The <Request /> component subscribes to these notifications and will trigger a reload if necessary if an invalidated request is in active use, letting you craft advanced policies that meet your product's needs.

WarpDrive provides a basic CachePolicy with a number of great defaults that is a great starting point for most applications. We configure this basic policy below.

The basic policy will invalidate requests based on caching and date headers available on request responses, falling back to a simple time based policy.

ts
import Store, { CacheHandler } from '@ember-data/store';
import type { CacheCapabilitiesManager } from '@ember-data/store/types';

import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import { CachePolicy } from '@ember-data/request-utils'; 

import JSONAPICache from '@ember-data/json-api';

import type { ResourceKey } from '@warp-drive/core-types';
import {
  instantiateRecord,
  registerDerivations,
  SchemaService,
  teardownRecord
} from '@warp-drive/schema-record';

export default class AppStore extends Store {

  requestManager = new RequestManager()
    .use([Fetch])
    .useCache(CacheHandler);

  lifetimes = new CachePolicy({ 
    apiHardExpires: 15 * 60 * 1000, // 15 minutes
    apiSoftExpires: 1 * 30 * 1000, // 30 seconds
    constraints: {
      'X-WarpDrive-Expires': true,
      'Cache-Control': true,
      'Expires': true,
    }
  });

  createSchemaService() {
    const schema = new SchemaService();
    registerDerivations(schema);
    return schema;
  }

  createCache(capabilities: CacheCapabilitiesManager) {
    return new JSONAPICache(capabilities);
  }

  instantiateRecord(identifier: ResourceKey, createArgs?: Record<string, unknown>) {
    return instantiateRecord(this, identifier, createArgs);
  }

  teardownRecord(record: unknown): void {
    return teardownRecord(record);
  }
}

Configure Your Framework

The final setup step is to configure reactivity for your framework. See each framework's guide for this step.

Configure ESLint

🚧 Under Construction

Released under the MIT License.