This tutorial demonstrates how to use TypeSpec to design and implement a RESTful JavaScript API application. TypeSpec is an open-source language for describing cloud service APIs and generates client and server code for multiple platforms. By following this tutorial, you'll learn how to define your API contract once and generate consistent implementations, helping you build more maintainable and well-documented API services.
In this tutorial, you:
[!div class="checklist"]
- Create a TypeScript API server application
- Define your API using TypeSpec
- Generate API code from TypeSpec definitions
- Implement service functionality with in-memory storage
- Integrate with Azure Cosmos DB for persistent storage
- Run and test your API locally
- An active Azure account. Create an account for free if you don't have one.
- Node.js LTS installed on your system.
- TypeScript for writing and compiling TypeScript code.
- TypeSpec compiler installed globally:
npm install -g @typespec/compiler
- Visual Studio Code with the TypeSpec extension
- Azure Cosmos DB account (for database integration)
TypeSpec helps you define your API in a language-agnostic way and generate server and client code for multiple platforms. This allows you to:
- Define your API contract once
- Generate consistent server and client code
- Focus on implementing business logic rather than API infrastructure
- OpenAPI definitions for your API
- Server-side middleware and routing code
- Client SDKs for consuming your API
- Type definitions for requests and responses
- Implementing service interfaces with business logic
- Integrating with data stores (like Azure Cosmos DB)
- Setting up build and deployment processes
- Hosting your API (locally or in Azure)
First, let's set up a basic TypeScript project:
- Create a project directory and initialize your package.json:
mkdir widget-api
cd widget-api
npm init -y- Install required dependencies:
npm install express swagger-ui-express yaml @typespec/compiler
npm install --save-dev typescript @types/express @types/node @types/swagger-ui-express- Create a basic
tsconfig.jsonfile:
{
"compilerOptions": {
"target": "ES2021",
"module": "NodeNext",
"moduleResolution": "nodenext",
"esModuleInterop": true,
"outDir": "dist",
"strict": true,
"sourceMap": true
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}Now, let's define our API with TypeSpec:
- Create a spec directory and add the required files:
mkdir -p spec- Create a
spec/main.tspfile to define your API:
import "@typespec/http";
using Http;
@service(#{ title: "Widget Service" })
namespace DemoService;
model Widget {
@visibility(Lifecycle.Read, Lifecycle.Update)
@path
id: string;
weight: int32;
color: "red" | "blue";
}
@error
model Error {
code: int32;
message: string;
}
@route("/widgets")
@tag("Widgets")
interface Widgets {
@get list(): Widget[] | Error;
@get read(@path id: string): Widget | Error;
@post create(...Widget): Widget | Error;
@patch update(...Widget): Widget | Error;
@delete delete(@path id: string): void | Error;
@route("{id}/analyze") @post analyze(@path id: string): string | Error;
}- Create a
spec/tspconfig.yamlfile to configure code generation for the OpenAPI spec, the client library and the API server library:
emit:
- "@typespec/openapi3"
- "@typespec/http-server-js"
options:
"@typespec/openapi3":
emitter-output-dir: "{project-root}/generated/spec/openapi3"
"@typespec/http-server-js":
emitter-output-dir: "{project-root}/generated/server"
express: true
omit-unreachable-types: true- Add a script entry to your
package.jsonto compile the TypeSpec definitions:
"build:tsp": "tsp compile ./spec --config ./spec/tspconfig.yaml"- Run the build script to generate your code:
npm run build:tspThis will create:
- OpenAPI specifications in
generated/spec/openapi3/openapi.yaml - API server routes (also known as middleware) in
generated/server/. The key integration file isgenerated/server/src/generated/http/router.ts. - Client library, to call API server routes, in
generated/client/. The key integration file is/generated/client/src/widgetServiceClient.ts.
Create a basic Express.js JavaScript API server.
-
Create a
serverdirectory for your application code:mkdir -p server
-
Create the
./server/server.tsfile for the API server:import express from "express"; import swaggerUi from 'swagger-ui-express'; import YAML from 'yaml'; import path from 'path'; import { promises as fs } from 'fs'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); // Add generated OPENAPI spec const swaggerSpecPath = path.resolve(__dirname, 'FILE-NAME'); console.log(`Loading swagger spec from ${swaggerSpecPath}`); const swaggerSpec = await fs.readFile(swaggerSpecPath, 'utf8'); const swaggerDocument = YAML.parse(swaggerSpec) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // Add generated route const thisWidgetService = new MyWidgetService(); const router = createWidgetServiceRouter(thisWidgetService); app.use(ROUTE-METHOD); app.listen(port, () => { console.log("Server listening on http://localhost:port"); });
-
Replace
FILE-NAMEwith the generated OpenAPI spec file found at/generated/spec/openapi3. -
Replace
ROUTE-METHODwith the generated route for the API found at/generated/server/src/generated/http/router.js. The method name iscreateWidgetServiceRouter. The entire change is:app.use(router.expressMiddleware);
The generated server route provides an interface but not the actual business logic to fill in the methods of the interface. You need to create the implementation for that interface. This implementation creates an in-memory data store for CRUD operations associated with the REST specification.
- Create
server/widgetService.tsto implement the service interface:
import type { Widget, WidgetServiceError, WidgetService } from '../generated/server/src/generated/models/all/widget-service.js';
import type { WidgetUpdate, ResourceDeletedResponse, WidgetCreate, WidgetCollectionWithNextLink } from '../generated/server/src/generated/models/all/typespec/rest/resource.js';
export default class MyWidgetService implements WidgetService {
private widgets: Widget[] = [];
async get(ctx: unknown, id: string): Promise<Widget | WidgetServiceError> {
const widget = this.widgets.find((w) => w.id === id);
if (!widget) {
return { code: 404, message: 'Widget not found' };
}
return widget;
}
async update(ctx: unknown, id: string, properties: WidgetUpdate): Promise<Widget | WidgetServiceError> {
const widgetIndex = this.widgets.findIndex((w) => w.id === id);
if (widgetIndex === -1) {
return { code: 404, message: 'Widget not found' };
}
this.widgets[widgetIndex] = { ...this.widgets[widgetIndex], ...properties };
return this.widgets[widgetIndex];
}
async delete(ctx: unknown, id: string): Promise<ResourceDeletedResponse | WidgetServiceError> {
const widgetIndex = this.widgets.findIndex((w) => w.id === id);
if (widgetIndex === -1) {
return { code: 404, message: 'Widget not found' };
}
this.widgets.splice(widgetIndex, 1);
return { status: 'deleted', id };
}
async create(ctx: unknown, resource: WidgetCreate): Promise<Widget> {
const newWidget: Widget = {
id: (this.widgets.length + 1).toString(),
...resource,
};
this.widgets.push(newWidget);
return newWidget;
}
async list(ctx: unknown): Promise<WidgetCollectionWithNextLink | WidgetServiceError> {
return {
items: this.widgets,
nextLink: null
};
}
}Now, let's modify our service to use Azure Cosmos DB for persistent storage:
- Install the Cosmos DB SDK:
npm install @azure/cosmos- Create
server/widgetServiceCosmosDb.ts:
import { CosmosClient } from '@azure/cosmos';
import type { Widget, WidgetServiceError, WidgetService } from '../generated/server/src/generated/models/all/widget-service.js';
import type { WidgetUpdate, ResourceDeletedResponse, WidgetCreate, WidgetCollectionWithNextLink } from '../generated/server/src/generated/models/all/typespec/rest/resource.js';
export default class MyWidgetServiceCosmosDb implements WidgetService {
private client: CosmosClient;
private container: any;
constructor() {
const endpoint = process.env.COSMOS_DB_ENDPOINT || '';
const key = process.env.COSMOS_DB_KEY || '';
const databaseId = process.env.COSMOS_DB_DATABASE_ID || 'WidgetsDb';
const containerId = process.env.COSMOS_DB_CONTAINER_ID || 'Widgets';
this.client = new CosmosClient({ endpoint, key });
this.container = this.client.database(databaseId).container(containerId);
}
async get(ctx: unknown, id: string): Promise<Widget | WidgetServiceError> {
try {
const { resource } = await this.container.item(id).read();
if (!resource) {
return { code: 404, message: 'Widget not found' };
}
return resource;
} catch (error) {
return { code: 500, message: 'Error retrieving widget' };
}
}
async update(ctx: unknown, id: string, properties: WidgetUpdate): Promise<Widget | WidgetServiceError> {
try {
const { resource } = await this.container.item(id).read();
if (!resource) {
return { code: 404, message: 'Widget not found' };
}
const updatedWidget = { ...resource, ...properties };
const { resource: updated } = await this.container.items.upsert(updatedWidget);
return updated;
} catch (error) {
return { code: 500, message: 'Error updating widget' };
}
}
async delete(ctx: unknown, id: string): Promise<ResourceDeletedResponse | WidgetServiceError> {
try {
await this.container.item(id).delete();
return { status: 'deleted', id };
} catch (error) {
return { code: 500, message: 'Error deleting widget' };
}
}
async create(ctx: unknown, resource: WidgetCreate): Promise<Widget> {
const newWidget: Widget = {
id: (Date.now().toString()), // Generate a unique ID
...resource,
};
const { resource: created } = await this.container.items.create(newWidget);
return created;
}
async list(ctx: unknown): Promise<WidgetCollectionWithNextLink | WidgetServiceError> {
try {
const { resources } = await this.container.items.readAll<Widget>().fetchAll();
return {
items: resources,
nextLink: null,
};
} catch (error) {
return { code: 500, message: 'Error listing widgets' };
}
}
}- Update your
server/server.tsto use the Cosmos DB implementation:
// In server.ts, change this line:
import MyWidgetService from "./widgetService.js";
// To use the Cosmos DB implementation:
import MyWidgetServiceCosmosDb from "./widgetServiceCosmosDb.js";
// And update the service instantiation:
const thisWidgetService = new MyWidgetServiceCosmosDb();- Before running, set up the environment variables for Cosmos DB:
# For Windows
set COSMOS_DB_ENDPOINT=your_cosmos_db_endpoint
set COSMOS_DB_KEY=your_cosmos_db_key
# For macOS/Linux
export COSMOS_DB_ENDPOINT=your_cosmos_db_endpoint
export COSMOS_DB_KEY=your_cosmos_db_key- Build and start your application:
npm run build
npm start- Your API is now running at http://localhost:8080 with Swagger UI at http://localhost:8080/api-docs
You can deploy this application to Azure using Azure Container Apps:
- Create an Azure Container Registry
- Build and push your Docker image
- Deploy to Azure Container Apps using the Azure Developer CLI:
azd upOnce deployed, you can:
- Access the Swagger UI to test your API
- Create, read, update, and delete widgets through the API
- Use the generated client SDK in another application to consume your API
When you're done with this tutorial, you can clean up the Azure resources:
azd downOr delete the resource group directly from the Azure portal.
- For any issues with the procedure, create an issue on the sample code repository