Have you ever needed to switch your backend endpoints before building for production? Or has it been problematic to change the endpoint of a resource? It sure happened to me, so I am going to show you how I dealt with this issue. If you just want to browse the final code, you can check it here.

What is the problem?

In our code base we have a general data service, that slightly abstracts the HttpClientService provided by Angular. This data service allows us to not worry about the backend endpoint — aside from development and production, we have multiple backends — and the default content type. But from time to time we refactor the endpoint of a specific resource, for example we go from postto posts. What happens next? Since we are using a general data service, our components and services directly add the resource path: this._dataService.getAll<Post[]>('post') .

Now we have to track down every instance that calls this resource. But we cannot use the IDE’s refactoring tools, since we are talking about plain strings. Not only that but there are multiple cases we need to search:

  • 'post, with only one 'because we might have 'post/paginated
  • `post, because we might use both 'and `to type our strings
  • "post, for the same reason

And hope we always type it and never get the path from some other call.
This process is time-consuming, error prone, and hard to automate. So…

What do we want?

First, we want to use the environment files to get the endpoints. With this approach we can switch environment files, using angular.json’s replace field, for all the stages of our project — development, test, staging, production, etc.

Second, we want to abstract a general data service, that gets the backend endpoint, into several specific services, that add the resource path. Whenever in the future we want to refactor a resource, we only need to update the path in this specific service.

First step: Inject the environment into the data service

An Angular project usually has different environment files, one for development and one for production, for example.

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

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

And we want to use the baseUrl in our general data service. We could directly import the environment file into the data service, but we would probably end up with deeply nested imports (i.e.: ../../../../app/environments/environment). Alternatively, if we are using lazy loaded modules or the Nx monorepo file structure, we want to keep the project structure separated.

Let us start from the data service. It should have an apiUrl with the backend endpoint, which comes from the environment.

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

As we mentioned, we want to avoid a direct import of the environment file. The option we went with is injecting it.

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

Where EnvironmentConfig has the fields we are interested in from the environment.

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

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

To be able to inject the configuration, we need to declare it as a provider in the module. We use the forRoot technique, returning a ModuleWithProviders.

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

And finally, in the application module, which resides in the same root folder as the environment files, we import HttpModule using the forRoot function we implemented.

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

If now we were to inject the data service in a component and check its apiUrl attribute we would get the values from the environment file.

Second step: Use specific services

This step is quite intuitive. We only need to inject the data service into our domain specific services.

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

If we choose to, we can implement generic HTTP calls in our data service, and then we specify them in our resource data service.

// 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 usage we abstract from the components the backend endpoint — used by the general data service, the resource endpoint — used by the specific resource service, and the typing of each call — defined by each call in the specific resource service.

// 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);
  }
}

Conclusion

You can see all the code in this live example.

This approach can be used for different project structures and for lazy loaded modules, so you should be able to easily accommodate it to your needs.

I hope this article is of use to you. If there is some mistake or you want to add anything, leave a comment. I will be delighted to read them 😀.