Skip to content

Ember Adapters — Configuring API Communication

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Ember Adapters. We cover key concepts, practical examples, and best practices to help you master this topic.

Ember adapters determine how Ember Data communicates with your backend. They define the URL structure, request headers, query parameters, and error handling. The default JSONAPIAdapter works with JSON:API backends, but custom adapters support any API format.

What You'll Learn

You will learn how to configure adapters, customize URLs, add authentication headers, handle errors, and create per-model adapters.

Why It Matters

Every backend is different. Adapters isolate API concerns from your application logic. When the API changes, you update the adapter, not every route and component.

Real-World Use

A security platform communicates with a REST API that uses custom headers for API keys, Rate Limiting, and response formats. A custom adapter handles authentication tokens, retry logic, and error normalization so the rest of the application remains clean.

flowchart LR
    A[Store] --> B[Adapter]
    B --> C[URL Builder]
    B --> D[Header Builder]
    B --> E[Error Handler]
    C --> F[HTTP Request]
    D --> F
    F --> G[Response]
    G --> H[Serializer]
    H --> A

Default Adapter

Ember generates a default adapter at app/adapters/application.js.

// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class ApplicationAdapter extends JSONAPIAdapter {
  // Default namespace
  namespace = 'api/v1';
}

This configures all requests to go to /api/v1/posts, /api/v1/users, etc.

Customizing URLs

Override URL methods to customize how endpoints are built.

// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class ApplicationAdapter extends JSONAPIAdapter {
  host = 'https://api.example.com';
  namespace = 'v2';

  // Custom URL for findAll
  urlForFindAll(modelName) {
    if (modelName === 'analytics') {
      return `${this.host}/${this.namespace}/dashboard/stats`;
    }
    return super.urlForFindAll(modelName);
  }

  // Custom URL for query
  urlForQuery(query, modelName) {
    if (modelName === 'search') {
      return `${this.host}/${this.namespace}/search`;
    }
    return super.urlForQuery(query, modelName);
  }

  // Custom URL for createRecord
  urlForCreateRecord(modelName, snapshot) {
    if (modelName === 'order') {
      return `${this.host}/${this.namespace}/checkout/orders`;
    }
    return super.urlForCreateRecord(modelName, snapshot);
  }
}

Adding Headers

Add authentication tokens and custom headers.

// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';
import { inject as service } from '@ember/service';

export default class ApplicationAdapter extends JSONAPIAdapter {
  @service session;

  get headers() {
    let headers = {
      'Content-Type': 'application/vnd.api+json',
      'Accept': 'application/vnd.api+json'
    };

    if (this.session.isAuthenticated) {
      headers['Authorization'] = `Bearer ${this.session.accessToken}`;
      headers['X-Organization'] = this.session.organizationId;
    }

    return headers;
  }
}

Per-Model Adapters

Override the adapter for specific models.

// app/adapters/analytics.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class AnalyticsAdapter extends JSONAPIAdapter {
  namespace = 'analytics/v1';

  // Analytics data is read-only
  urlForFindAll() {
    return '/analytics/v1/dashboard';
  }

  // Disable create/update/delete
  createRecord() {
    throw new Error('Analytics records are read-only');
  }
}

Custom Query Parameters

// app/adapters/post.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class PostAdapter extends JSONAPIAdapter {
  namespace = 'api/v1';

  // Add custom query params
  queryParamsSerializer(query) {
    // Convert Ember query format to API format
    let params = {};

    if (query.filter) {
      Object.keys(query.filter).forEach(key => {
        params[`filter[${key}]`] = query.filter[key];
      });
    }

    if (query.sort) {
      params.sort = query.sort;
    }

    if (query.page) {
      params['page[size]'] = query.page.size;
      params['page[number]'] = query.page.number;
    }

    if (query.include) {
      params.include = query.include;
    }

    return params;
  }

  // Override query URL
  urlForQuery(query, modelName) {
    let url = super.urlForQuery(query, modelName);

    if (query.category) {
      url = `/api/v1/categories/${query.category}/posts`;
    }

    return url;
  }
}

Error Handling

Customize error responses from the server.

// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';
import { inject as service } from '@ember/service';

export default class ApplicationAdapter extends JSONAPIAdapter {
  @service toast;

  handleResponse(status, headers, payload, requestData) {
    if (status === 401) {
      // Token expired — redirect to login
      this.session.invalidate();
      return;
    }

    if (status === 422 && payload.errors) {
      // Validation errors — let Ember Data handle
      return this._super(status, headers, payload, requestData);
    }

    if (status >= 500) {
      // Server error — show notification
      this.toast.show('Server error. Please try again.', 'danger');
      console.error('API Error:', status, payload);
    }

    return this._super(...arguments);
  }
}

Caching Headers

Control caching behavior.

// app/adapters/post.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class PostAdapter extends JSONAPIAdapter {
  namespace = 'api/v1';

  // Add cache headers for findAll
  urlForFindAll(modelName) {
    let url = super.urlForFindAll(modelName);
    return `${url}?cache_bust=${Date.now()}`;
  }
}

// Or use ETags
get headers() {
  return {
    'If-None-Match': this.session.lastETag
  };
}

Rate Limiting and Retry

// app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';

export default class ApplicationAdapter extends JSONAPIAdapter {
  maxRetries = 3;
  retryDelay = 1000; // ms

  async ajax(url, method, options) {
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        return await super.ajax(url, method, options);
      } catch (error) {
        if (error.status === 429 && attempt < this.maxRetries) {
          // Rate limited — wait and retry
          let delay = this.retryDelay * Math.pow(2, attempt);
          console.warn(`Rate limited. Retrying in ${delay}ms...`);
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }
        throw error;
      }
    }
  }
}

Common Mistakes

  1. Hardcoding host strings. Use config/environment.js for API URLs. Different environments need different hosts.
  2. Not calling super when overriding methods. Overriding urlForFindAll without falling back to super for unspecified model types breaks other models.
  3. Mutating headers instead of returning a new object. The headers property should return a new object each time. Mutating a cached object causes stale auth tokens.
  4. Forgetting CORS configuration. Ember apps at localhost:4200 calling api.example.com need CORS headers. Configure your backend accordingly.
  5. Not handling 401 globally. Every request can fail with 401. Handle it in the application adapter once instead of in every route.

Practice Questions

  1. What is the purpose of an Ember adapter?
  2. How do you add authentication headers to all requests?
  3. How do you create a per-model adapter?
  4. How do you customize error handling for different HTTP status codes?
  5. Challenge: Create an application adapter that: (1) adds an API key header from the session service, (2) prepends /api/v2/ to all URLs, (3) handles 429 rate limiting with exponential backoff, (4) redirects to login on 401, and (5) logs 500 errors with a toast notification. Create a per-model adapter for analytics that uses a different host.

FAQ

Can I use multiple adapters in one app?

Yes. Each model can have its own adapter. Unspecified models use the application adapter.

What is the default adapter?

JSONAPIAdapter. It expects a JSON:API compliant backend.

How do I change the API base URL per environment?

Set host in config/environment.js and reference it from the adapter.

Can adapters handle file uploads?

Yes. Override ajax to use FormData for specific models.

What happens if an adapter method throws?

Ember Data catches the error and sets the record to isError: true.

Mini Project

Create a complete adapter configuration for a multi-environment application. (1) Application adapter with JWT auth headers, rate limiting retry, and global error handling. (2) Analytics adapter pointing to a separate analytics API. (3) Legacy adapter for a REST API without JSON:API (override pathForType, urlForFindAll). (4) Search adapter that transforms Ember query format to the search API format.

What's Next

Now that you understand adapters, learn Ember Serializers for data format handling. Then explore Ember Services for shared application state.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro