Generating Data Access Code for Angular with OpenAPI Specs

When developing a front-end application with Angular and an existing client, you need to program models and services for data access. This is a repetitive task, complicated by changes in the back-end. One way to automate this is to use a code generator that runs on the OpenAPI spec of the back-end. But before going into the details of that automated process, I would like to highlight an approach without having an OpenAPI spec.

Coding without an OpenAPI spec

This approach requires knowledge of the back-end code. Let’s imagine we have a .NET Core login controller that returns a user object on success.

// User
public record User(string Username, string FullName);

// Login request
public record LoginRequest(string Username, string Password);

// Controller, without the using statements
[ApiController]
[Route("api/login")]
public class LoginController : ControllerBase
{
    [HttpPost]
    public IActionResult Login([FromBody] LoginRequest request)
    {
        User user = new(request.Username, "John Doe");
        return Ok(user);
    }
}

To access this endpoint with an Angular client, we need the models (User, LoginRequest) and a service that makes the actual HTTP request. To help with model creation, there is the C# to TypeScript plugin for Visual Studio Code.

// User model
export interface User {
  username: string;
  fullName: string;
}

// Login request model
export interface LoginRequest {
  username: string;
  password: string;
}

// Service (without the imports)
@Injectable({ providedIn: "root" })
export class LoginService {
  private readonly http = inject(HttpClient);

  public login(loginRequest: LoginRequest): Observable<User> {
    const url = `${baseUrl}/api/login`;
    return this.http.post<User>(url, loginRequest);
  }
}

This needs to be done for all endpoints, resulting in tedious, repetitive work. And whenever something changes, you need to adapt these changes in your front-end code.

OpenAPI Code Generator

One approach to automate this is the use of a code generator. The input is an OpenAPI spec of the back-end. In a newly scaffolded .NET 10 web API project, OpenAPI is already included and can be reached at https://localhost:PORT/openapi/v1.json in development mode. For .NET version before 9, the tool was called Swagger and has a different output location.

In our little example, the login controller is creating several entries in the spec.

{
  "openapi": "3.1.1",
  "info": {
    "title": "DotNetWebApiDemo | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://localhost:PORT/"
    }
  ],
  "paths": {
    "/api/login": {
      "post": {
        "tags": ["Login"],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "LoginRequest": {
        "required": ["username", "password"],
        "type": "object",
        "properties": {
          "username": {
            "type": "string"
          },
          "password": {
            "type": "string"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Login"
    }
  ]
}

But the spec is missing one decisive detail, the return types are not included. It just says that status code 200 is returned. The reason for this behavior is that the return type is not defined in the controller. It uses the IActionResult interface, which does not include the expected return type. We therefore need to specify the return type explicitly. This can be done with ActionResult<T>, ProduceResponseType attributes or TypedResults.

In our example, we will use the very basic ActionResult<T> version. If your controller also returns other types of responses (like BadRequest), you need to use one of the other approaches.

// Controller, without the using statements
[ApiController]
[Route("api/login")]
public class LoginController : ControllerBase
{
    [HttpPost]
    public ActionResult<User> Login([FromBody] LoginRequest request)
    {
        User user = new(request.Username, "John Doe");
        return Ok(user);
    }
}

This results in a OpenAPI spec including the return type.

{
  ...
  "paths": {
    "/api/login": {
      "post": {
        ...
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      ...
      "User": {
        "required": ["username", "fullName"],
        "type": "object",
        "properties": {
          "username": {
            "type": "string"
          },
          "fullName": {
            "type": "string"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Login"
    }
  ]
}

With this spec, OpenAPI generators like ng-openapi or orval can generate models and data access services. This makes life much easier.