The challenge

At some point, most of us have faced the need to swap backend endpoints when shipping to production. Perhaps changing a resource URL has also caused friction. That has definitely been my experience, and here I will walk through the solution I adopted. If you would like to inspect the complete implementation, a live demo is available here.

Why it is painful

Our codebase relies on a shared data service that wraps Angular's HttpClientService with a thin abstraction. That service abstracts away backend specifics—we deal with multiple backends besides dev and prod—and also standardizes content types. Still, from time to time, we rename a resource path, say from post to posts. The trouble begins because components and services call the generic service directly with the resource string concatenated, like this: this._dataService.getAll<Post[]>('post').

Locating every reference becomes a manual hunt. IDE refactoring is useless here, as these are plain strings. We also have to account for several variations:

  • 'post, since a single quote might precede 'post/paginated
  • `post, for template literals
  • "post, for double-quoted strings

And we must pray the path was never derived dynamically from another function call.
It is tedious, prone to mistakes, and tough to automate. So...

What we aim for

The first goal is pulling endpoints from environment files. That way, we can swap configurations for each stage—dev, test, staging, prod—using the replace option in angular.json.

The second goal is to split the generic data service, which holds the backend URL, into resource-specific services that append their own path. A future rename then only touches one dedicated service.

Step 1: Injecting the environment into the data service

Angular projects typically include environment files per stage, at least one for development and one for production.

// environment.ts
export const environment = {
  production: false,
  baseUrl: 'http://localhost:3333'
};

// environment.prod.ts
export const environment = {
  production: true,  
  baseUrl: 'https://jsonplaceholder.typicode.com'
};

The idea is to supply baseUrl to our general data service. A straightforward import of the environment might seem fine, but it introduces long relative paths like ../../../../app/environments/environment. Moreover, with lazy loading or an Nx workspace, keeping concerns separated matters more.

So the data service should receive an apiUrl that holds the backend root, pulled from the environment.

// data.service.ts
@Injectable({
  providedIn: 'root'
})
export class DataService {
  public apiUrl: string;
constructor(config: EnvironmentConfig) {
    this.apiUrl = `${config.environment.baseUrl}`;
  }
}

Avoiding the direct import is exactly why we chose injection.

// data.service.ts
@Injectable({
  providedIn: 'root'
})
export class DataService {
  public apiUrl: string;
constructor(@Inject(ENV_CONFIG) private config: EnvironmentConfig) {
    this.apiUrl = `${config.environment.baseUrl}`;
  }
}

EnvironmentConfig defines the fields we care about from the environment.

// environment-config.interface.ts
export interface EnvironmentConfig {
  environment: {
    baseUrl: string;
  };
}

export const ENV_CONFIG = new InjectionToken<EnvironmentConfig>('EnvironmentConfig');

To make this injectable, we register it as a provider at the module level. The forRoot pattern is our tool, yielding a ModuleWithProviders.

// http.module.ts
@NgModule({
  imports: [CommonModule]
})
export class HttpModule {
  static forRoot(config: EnvironmentConfig): ModuleWithProviders<HttpModule> {
    return {
      ngModule: HttpModule,
      providers: [
        {
          provide: ENV_CONFIG,
          useValue: config
        }
      ]
    };
  }
}

At the application module—situated alongside the environment files—we import HttpModule via our forRoot implementation.

// app.module.ts
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    AppRoutingModule,
    HttpModule.forRoot({ environment })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Once done, injecting the data service into any component and reading its apiUrl reveals the environment-specific values.

Step 2: Introducing domain-specific services

This stage is straightforward: bring the generic data service into each resource-specific service via DI.

// posts-req.service.ts
@Injectable({
  providedIn: 'root'
})
export class PostsReqService {
  constructor(private data: DataService) {}
}

Optionally, the generic service can expose basic HTTP verbs, and the resource service decides which ones to expose.

// data.service.ts
@Injectable({
  providedIn: 'root'
})
export class DataService {
  public apiUrl: string;

  constructor(@Inject(ENV_CONFIG) private config: EnvironmentConfig, private http: HttpClient) {
    this.apiUrl = `${config.environment.baseUrl}`;
  }

  getAll<T>(path: string): Observable<T> {
    return this.http.get<T>(`${this.apiUrl}/${path}`);
  }
}

// posts-req.service.ts
@Injectable({
  providedIn: 'root'
})
export class PostsReqService {
  
  constructor(private data: DataService) {}

  getAllPosts(limit: number): Observable<Post[]> {
    return this.data
      .getAll<Post[]>(`posts`)
      .pipe(map(ret => ret.slice(0, limit)));
  }
}

With this arrangement, components no longer see the backend URL—handled by the generic service—nor the resource path—managed by the specific service—nor the generic typing—each call in the resource service defines it.

// posts.component.ts
@Component({
  selector: 'app-posts',
  templateUrl: './posts.component.html',
  styleUrls: ['./posts.component.css']
})
export class PostsComponent implements OnInit {
  posts$: Observable<Post[]>;

  constructor(private postsReqService: PostsReqService) {}

  ngOnInit() {
    this.posts$ = this.postsReqService.getAllPosts(10);
  }
}

Wrapping up

The full code is available in this live example.

Whether you use a standard structure or lazy loading, this pattern is adaptable and should fit your setup with minor tweaks.

I hope you find this useful. Should you spot any issues or have suggestions, feel free to comment below—I look forward to reading them.