API first with TypeSpec, OpenAPI, React/Tanstack and Microsoft Dynamics 365 Business Central, part 1
DEC 14, 2025
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.