Keeping frontend and backend in sync is a persistent challenge in software development. The API-first pattern addresses this by establishing the contract before implementation. With TypeSpec, an API description language created by Microsoft, we can write our API definition once, and automatically generate OpenAPI specs, TypeScript types, and runtime validation.

This approach ensures type safety throughout the development stack, catching API changes at compile time rather than runtime.

Understanding the Business Central Sales Order API

Business Central exposes comprehensive REST APIs out of the box. The Sales Order endpoint provides access to sales header and line item data. For complete order information, use the $expand query parameter:

GET .../salesOrders?$expand=salesOrderLines

Sample API response

The endpoint returns structured JSON containing order details and associated line items:

{
  "value": [
    {
      "id": "f1686ae7-c8ab-ed11-aada-000d3a298ab3",
      "number": "1001",
      "externalDocumentNumber": "PO-12345",
      "orderDate": "2024-12-01",
      "customerNumber": "10000",
      "customerName": "Adatum Corporation",
      "salesperson": "PS",
      "requestedDeliveryDate": "2024-12-15",
      "totalAmountExcludingTax": 5250.00,
      "totalTaxAmount": 420.00,
      "totalAmountIncludingTax": 5670.00,
      "fullyShipped": false,
      "status": "Draft",
      "salesOrderLines": [
        {
          "id": "a2b3c4d5-c8ab-ed11-aada-000d3a298ab3",
          "documentId": "f1686ae7-c8ab-ed11-aada-000d3a298ab3",
          "sequence": 10000,
          "lineType": "Item",
          "lineObjectNumber": "1896-S",
          "description": "ATHENS Desk",
          "unitOfMeasureCode": "PCS",
          "unitPrice": 1500.00,
          "quantity": 2,
          "amountExcludingTax": 3000.00,
          "taxPercent": 8,
          "totalTaxAmount": 240.00,
          "amountIncludingTax": 3240.00,
          "shipmentDate": "2024-12-15"
        },
        {
          "id": "b3c4d5e6-c8ab-ed11-aada-000d3a298ab3",
          "documentId": "f1686ae7-c8ab-ed11-aada-000d3a298ab3",
          "sequence": 20000,
          "lineType": "Item",
          "lineObjectNumber": "1900-S",
          "description": "PARIS Guest Chair",
          "unitOfMeasureCode": "PCS",
          "unitPrice": 750.00,
          "quantity": 3,
          "amountExcludingTax": 2250.00,
          "taxPercent": 8,
          "totalTaxAmount": 180.00,
          "amountIncludingTax": 2430.00,
          "shipmentDate": "2024-12-15"
        }
      ]
    }
  ]
}

Project setup

Initial installation

Create a project directory and install the required dependencies:

npm init -y
npm install --save-dev @typespec/compiler @typespec/http @typespec/rest @typespec/openapi3

Directory structure and configuration

Create the api-doc directory and configuration file:

mkdir api-doc
cd api-doc
vim src/main.tsp # wq!
vim tspconfig.yaml

tspconfig.yaml:

emit:
  - "@typespec/openapi3"
options:
  "@typespec/openapi3":
    emitter-output-dir: "{cwd}/public/definition"

NPM scripts

Update package.json with TypeSpec compilation scripts:

{
  "name": "api-doc",
  "version": "1.0.0",
  "scripts": {
    "tsp:compile": "tsp compile ./api-doc/src/main.tsp",
    "tsp:watch": "tsp compile ./api-doc/src/main.tsp --watch",
    "tsp:format": "tsp format '**/*.tsp'"
  },
  "type": "commonjs",
  "devDependencies": {
    "@typespec/compiler": "^1.7.0",
    "@typespec/http": "^1.7.0",
    "@typespec/openapi3": "^1.7.0",
    "@typespec/rest": "^0.77.0"
  }
}

Execute formatting and compilation:

npm run tsp:format
npm run tsp:compile

The command generates the OpenAPI definition at <project-name>/public/definition/openapi.yaml.

Creating TypeSpec models

File setup

Create the SalesOrder TypeSpec file:

touch ./api-doc/src/SalesOrder.tsp

Model definition

Open the file and define the API models and interface:

import "@typespec/http";
import "@typespec/openapi";

using TypeSpec.Http;
using TypeSpec.OpenAPI;

@service(#{ title: "Business Central Sales Order API" })
@server("https://api.businesscentral.dynamics.com/v2.0/<tenantId>/<env>/", "Business Central API Server")
namespace hpeide.Sales {
  model SalesLine {
    id: string;
    sequence: int32;
    itemId: string;
    description: string;
    quantity: decimal;
    unitPrice: decimal;
    amountExcludingTax: decimal;
    totalTaxAmount: decimal;
    amountIncludingTax: decimal;
  }

  model SalesOrder {
    id: string;
    number: string;
    orderDate: plainDate;
    customerName: string;
    salesperson: string;
    totalAmountExcludingTax: decimal;
    totalTaxAmount: decimal;
    totalAmountIncludingTax: decimal;
    salesOrderLines?: SalesLine[];
  }

  model SalesOrderResponse {
    @encodedName("application/json", "@odata.context")
    odataContext: string;

    @encodedName("application/json", "@odata.count")
    odataCount?: int32;

    value: SalesOrder[];
  }

  @route("/companies({companyId})/salesOrders")
  @tag("Sales")
  interface ISalesOrder {
    @get
    @operationId("listSalesOrders")
    @summary("Returns list of Sales Orders")
    list(
      @path
      @doc("Id for company")
      @format("uuid")
      companyId: string,

      @query
      @doc("Expand related entities")
      $expand?: "salesOrderLines",
    ): SalesOrderResponse;
  }
}

Importing models in main

Update src/main.tsp to import the SalesOrder models:

import "./SalesOrder.tsp";

Compilation

Run the formatting and compilation commands:

npm run tsp:format
npm run tsp:compile

The TypeSpec compiler generates an OpenAPI specification document that can be consumed by frontend tooling to maintain type safety.

What's next

The API contract is defined and the OpenAPI specification generated. Part 2 of this series explores creating a modern React frontend with TanStack Query to consume this API while maintaining end-to-end type safety throughout the application.

Resources