Ember Adapters — Configuring API Communication
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
- Hardcoding host strings. Use
config/environment.jsfor API URLs. Different environments need different hosts. - Not calling
superwhen overriding methods. OverridingurlForFindAllwithout falling back to super for unspecified model types breaks other models. - Mutating headers instead of returning a new object. The
headersproperty should return a new object each time. Mutating a cached object causes stale auth tokens. - Forgetting CORS configuration. Ember apps at localhost:4200 calling api.example.com need CORS headers. Configure your backend accordingly.
- Not handling 401 globally. Every request can fail with 401. Handle it in the application adapter once instead of in every route.
Practice Questions
- What is the purpose of an Ember adapter?
- How do you add authentication headers to all requests?
- How do you create a per-model adapter?
- How do you customize error handling for different HTTP status codes?
- 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 foranalyticsthat uses a different host.
FAQ
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