Angular Schematics – Generate an Http Service

We all know about the value of ng g c HeaderComponent. The powerful angular schematics are part of basically every Angular developer’s repertoire. But still we often tend to create those components, services, pipes, etc. and then always add the same code to the generated files:

  • We generate a dumb component and add the ChangeDetectionStrategy.OnPush manually
  • A container component is generated but the store field is added manually to the constructor and the ngOnDestroy to unsubscribe from Observables too
  • The angular CLI generates a service for us but when we want to add an HTTP request to that service manual work is involved

In this article we want to tackle the last point of the list above: using angular schematics to generate HTTP services. Generally, there are two main approaches:

  • Extending the schematic for services used in the Angular CLI
  • Creating a new schematic on our own

Because we don’t really need much of what is generated in case of a normal service we go for the second option. If we’re interested in spec file generation or want to keep much of what is generated, the first option would be more fitting.

Basic Setup for Angular Schematics

Angular schematics should be hermetic and standalone. Therefore we will create a separate project where our schematics are developed. Later this project can be consumed as a dependency in those projects that want to generate http services.

To get started we install the angular-devkit schematics-cli globally.

npm i -g @angular-devkit/schematics-cli

We want to generate a general angular building blocks schematic project with the following command:

schematics blank --name=ng-blocks-factory

Now we’ve got a blank project where we can develop our schematics:

Most interesting for us are the following two files:

  • collection.json defines all the schematics available
  • ng-blocks-factory/index.ts is the main file for our first schematic

We will rename a bit. The ng-blocks-factory folder inside src is changed to http-service to better indicate what the first schematic is about (don’t forget to rename inside the collection.json file too).

In the project we install another dependency that will give us some useful methods when working with schematics:

npm i --save-dev @schematics/angular

So far so good. We have a project and a more or less empty schematic.

Define the Angular schematics input parameters for the http service

In our use case a developer wants to create an HTTP service. Let’s think a moment about the basic inputs. I guess we can start with a name and base url. If we follow REST principles these two arguments should already suffice to generate the needed methods in the http service.

Hence, we create the following schema.json for our http-service schematic. This schema.json defines the inputs.

{
  "$schema": "http://json-schema.org/schema",
  "$id": "HttpServiceSchema",
  "type": "object",
  "properties": {
    "path": {
      "type": "string",
      "format": "path",
      "visible": false
    },
    "name": {
      "type": "string",
      "$default": {
        "$source": "argv",
        "index": 0
      }
    },
    "url": {
      "type": "string",
      "$default": {
        "$source": "argv",
        "index": 1
      }
    }
  },
  "required": [
    "name",
    "url"
  ]
}

The file should be located in the http-service folder. Alongside url and name, we’ve also got a path input parameter. This path points to the location where the service should actually be created inside a project.

Afterwards we also need to update our collection.json to include the schema.json of the http-service schematic. We’re also updating the function name in the index.ts file. This function name is used to reference the factory in the schema.json (httpService in our case after #):

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "http-service": {
      "description": "Generate http services",
      "factory": "./http-service/index#httpService",
      "schema": "./http-service/schema.json"
    }
  }
}

Implementing the factory function

The index.ts contains the factory function used to create the files in our schematic. As a parameter it receives the options passed by the user (and defined in the schema.json). The basic version should look similar to that below.

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

export function httpService(_options: any): Rule {
    return (tree: Tree, _context: SchematicContext) => {
        return tree;
    };
}

We extend that file by the following parts:

  • We first check if we’re in an Angular CLI workspace. If so, we read the workspace config, i.e. angular.json
  • The options name and path are cleaned up and the path constructed based on the passed project (or default project).
  • The template sources are defined. They point to a files folder where our templates will be added later. In the template function we provide the options alongside the functions classify and dasherize. This ensures that those values and functions are available in our template.

This should be the result:

export function httpService(_options: any): Rule {
    return (tree: Tree, _context: SchematicContext) => {
        const workspaceConfigBuffer = tree.read('angular.json');
        if (!workspaceConfigBuffer) {
            throw new SchematicsException('Not an Angular CLI workspace! The angular.json file is missing');
        }

        const workspaceConfig = JSON.parse(workspaceConfigBuffer.toString());
        const projectName = _options.project || workspaceConfig.defaultProject;
        const project = workspaceConfig.projects[projectName];

        const defaultProjectPath = buildDefaultPath(project);
        const parsedPath = parseName(defaultProjectPath, _options.name);
        const {name, path} = parsedPath;

        const templateSource = apply(
            url('./files'),
            [
                template({
                    ..._options,
                    classify: strings.classify,
                    dasherize: strings.dasherize,
                    name,
                }),
                move(path),
            ],
        );

        return mergeWith(templateSource)(tree, _context)
    };
}

The @schematics/angular utility method buildDefaultPath didn’t really work in my case and threw an error (reg. projectType property). That’s why I implemented the method myself as follows:

function buildDefaultPath(project: any) {
    const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`;
    const projectDirName = project['projectType'] === 'application' ? 'app' : 'lib';
    return `${root}${projectDirName}`;
}

This is our index.ts file. Next we can create the template file for our service.

Creating the Template for our Http Service

Templates in schematics are used as a base for the generated files. For our service we create a file named __name@dasherize__.http-service.ts in our http-service/files folder. The __name__ part refers to the name option passed to the templates in the index.ts file. To have a nice name we use @dasherize to format, e.g. ShoppingCart to shopping-cart. Because in the end we want to have dashes in our file names.

The content of the file will look similar to normal Angular service files, but the option values are included by using the <%= %> syntax.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root',
})
export class <%= classify(name) %>HttpService {
    private readonly baseUrl = '<%= path %>';

    constructor(private http: HttpClient) {}

    public getAll(): Observable<<%= name %>[]> {
        return this.http.get<<%= name %>[]>(this.baseUrl);
    }

    public getById(id: number): Observable<<%= name %>> {
        return this.http.get<<%= name %>>(`${this.baseUrl}/${id}`);
    }
}

export interface <%= classify(name) %> {
    id: number;
    // other properties
}

As a start we are adding two GET calls to load either all resources or a specific resource identified through its ID. So, this template is created based on the following assumptions taking Customer as an example.

  • All customers are available under the base url defined by the path, e.g. /api/v1/customers
  • A specific customer is exposed under /api/v1/customers/<ID>
  • The returned object’s type is Customer

To have valid code we define the interface for the response type inside the http service file. In the end that should probably be moved to a separate file.

After the factory function we now also have our template ready and can start to test our schematic.

Test the Schematic in an Angular Project

To test the schematic we first have to build it:

npm run build

Afterwards we can link it locally from an Angular Project. This assumes that your Angular project is on the same folder level as your ng-blocks-factory schematic project.

npm i --save-dev ../ng-blocks-factory

The schematic project is installed and we can now execute the schematic:

ng generate ng-blocks-factory:http-service Customer /api/v1/customer

We could also skip the installing and just execute the schematic by specifying the path to the collection as well as adding the schematic name after the colon:

ng generate ../ng-blocks-factory/src/collection.json:http-service shared/services/Customer /api/v1/customer 

This generates an http service for us:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
    providedIn: 'root',
})
export class CustomerHttpService {
    private readonly baseUrl = '';

    constructor(private http: HttpClient) {}

    public getAll(): Observable<Customer[]> {
        return this.http.get<Customer[]>(this.baseUrl);
    }

    public getById(id: number): Observable<Customer> {
        return this.http.get<Customer>(`${this.baseUrl}/${id}`);
    }
}

export interface Customer {
    id: number;
    // other properties
}

We’ve got our service generated. So, in the future we can just use a simple command instead of copying an existing services or generating a normal service and adding all the http part.

Of course you should probably build and publish your schematic to have an easy integration into any project. Finally, you could also add POST, PUT, PATCH and DELETE methods to your template.

The source are available on GitHub.