The source code for this blog is available on GitHub.
Note
Top

Building APIs with TypeScript

Cover Image for Building APIs with TypeScript
Han Chen
Han Chen

API Handling in TypeScript

In this guide, we will be discussing how to handle APIs in TypeScript using the NextApiRequest and NextApiResponse interfaces. We will also be extracting the try-catch block outside of the function and define the order type to ensure type-safety.

First, add the NextApiRequest and NextApiResponse to the TypeScript file for typescript api

import { NextApiRequest, NextApiResponse } from "next";
import bigCommerceService from "@/services/bigCommerceService";

export default async function CreateOrder(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "POST") {
    try {
      const response = await bigCommerceService.post(`${process.env.BIGCOMMERCE_STORE_API_URL}/v2/orders`, req.body);
      return res.status(response.status).json(response.data);
    } catch (err) {
      return res.status(400).json({ message: err });
    }
  } else {
    return res.status(400).json({ message: "Wrong Action" });
  }
}

As we can see in the code above, the API request is being handled within the CreateOrder function. In the event of an error, it returns a response with a status code of 400 and a message indicating the error.

To make the code more maintainable and reusable, it's a good idea to extract the try-catch block into a separate function. In this case, we will define a new function called handleResponse:

import { NextApiRequest, NextApiResponse } from "next";
import bigCommerceService from "@/services/bigCommerceService";

export async function handleResponse<T>(promise: Promise<AxiosResponse<T>>, res: NextApiResponse) {
  try {
    const response = await promise;
    return res.status(response.status).json(response.data);
  } catch (error: any) {
    return res.status(error.response?.status || 400).json({ message: error });
  }
}

export default async function CreateOrder(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const response = bigCommerceService.post<Order.CreateOrderResponse>(
      `${process.env.BIGCOMMERCE_STORE_API_URL}/v2/orders`,
      req.body as Order.CreateOrderRequest
    );

    return handleResponse(response, res);
  } else {
    return res.status(400).json({ message: "Wrong Action" });
  }
}

As we can see, the handleResponse function takes in two parameters: promise and res. The promise parameter is the API response, and the res parameter is the NextApiResponse instance.

type with Promise

The code defines an async function called getAllProducts that returns a Promise that resolves to an array of Product.GetProductResponse objects. The function takes an optional parameter query with a default value of an empty string.

The function uses the bigCommerceService to make an HTTP GET request to retrieve an array of products. The response is destructured and stored in the products constant.

If products exists, the code maps over each product, performs some string manipulation on the product's custom_url.url property, and assigns the result back to the custom_url.url property.

Finally, the function returns the array of products.

export async function getAllProducts(
  query = ""
): Promise<Product.GetProductResponse[]> {
  const {
    data: { data: products },
  } = await bigCommerceService.get<HttpProductsResponse>(
    `${process.env.BIGCOMMERCE_STORE_API_URL}/v3/catalog/products${
      query && "?" + query
    }`
  );

  if (products) {
    products.map((prod) => {
      ...
  }

  return products;
}

API - Change Javascript into Typescript

To change an API file from JavaScript to TypeScript, you will need to perform the following steps:

  1. Find the request and response payload
  2. Copy the response payload to a separate file and use a tool like QuickType to generate a TypeScript file with the response payload
  3. Insert the generated TypeScript code into the appropriate location and name it based on the file name
  4. Change the target JavaScript file to TypeScript and add the necessary infrastructure, input, and output types.
  5. Update the code with input types as well as output types

This process will provide a more reliable and safe way of handling the API files in TypeScript.

Let’s take /api/bigCommerce/[id]/status. as an example


Consider the example /api/bigCommerce/[id]/status.

import bigCommerceService from "@/services/bigCommerceService";
import { nextConnectInstance } from "@/lib/nextConnect";
import { v2 } from "@/lib/bigCommerce";

const orderStatusRoute = nextConnectInstance;

orderStatusRoute.put(async (req, res) => {
  const response = await bigCommerceService.put(v2(`orders/${req.query.id}`), {
    status_id: req.body.status_id
  })

  res.status(response.status).json(response.data);
});

export default orderStatusRoute;

Find the Request and Response Payload. For the given example, the request payload might look like this:

{
  status_id: 12
}

To find the response payload, you can use ctrl + shift + p in Windows or command + shift + p in macOS.

Find the response payload

After finding the response payload, copy it to a separate file and name it properly. For this example, I named the file update-order-response.json.

Use the command ctrl + shift + p in Windows or command + shift + p in macOS and choose quicktype to JSON to generate a TypeScript file with the response payload. The file name will be QuickType.ts.

The code was generated using quicktype.io. You can try this tool as well, a screenshot is provided below.

img

Upsert the synthesized typescript stuff into proper genre. The name will be generated according to your filename. Include the generated TypeScript code into the appropriate location, and the name will be derived based on your filename.

img

Change the target JavaScript file to TypeScript, add the necessary infrastructure, input, and output types.

import bigCommerceService from "@/services/bigCommerceService";
import { nextConnectInstance } from "@/lib/nextConnect";
import { v2 } from "@/lib/bigCommerce";

const orderStatusRoute = nextConnectInstance;

orderStatusRoute.put(async (req, res) => {
  const response = await bigCommerceService.put<Order.UpdateOrderResponse>(v2(`orders/${req.query.id}`), {
    status_id: req.body.status_id
  } as { status_id: number })

  res.status(response.status).json(response.data);
});

export default orderStatusRoute;

In the above code, { status_id: number } is the input type and <Order.UpdateOrderResponse> is the output type.

That is the complete process for converting the API to TypeScript.

Rewrite Server Api in Typescript

This is an example of an API endpoint that fetches all products from a BigCommerce store and returns the response as JSON. The code is written in JavaScript and uses the async/await syntax to make an asynchronous request to the BigCommerce API.

import bigCommerceService from "@/services/bigCommerceService";
import { httpsErrorHandler } from "@/lib/ApiHandler";
export default async function getAllProducts(req, res) {
  if (req.method !== "GET")
    return res.status(405).json({ message: "Method not allowed." });

  try {
    const {
      data: { data },
      status,
      // NOTE: Convert to BIGCOMMERCE_PRODUCTION_STORE_API_URL and BIGCOMMERCE_PRODUCTION_TOKEN inside bigCommerceService if want to retrieve production products
    } = await bigCommerceService.get(
      `${process.env.BIGCOMMERCE_STORE_API_URL}/v3/catalog/products?include_fields=name,is_visible,custom_url,inventory_level,price&include=variants`
    );

    return res.status(status).json(data);
  } catch (err) {
    httpsErrorHandler(res, err);
  }
}

This code is an revised version of a TypeScript module that exports a function called handleResponse, which takes three arguments: a Promise that resolves to an AxiosResponse, a NextApiResponse object, and an optional string dataKey. The purpose of this function is to handle the response from an HTTP request by extracting the data from the response object and sending it as a JSON response to the client.

The getAllProducts function in the "after" code makes an HTTP request to the BigCommerce API using the bigCommerceService.get method and passes the returned Promise to the handleResponse function along with the NextApiResponse object and the string 'data'. This tells the handleResponse function to extract the data property from the response object and return it as the JSON response. This is a cleaner and more modular approach than the "before" code because the handleResponse function can be used in multiple API endpoints to handle responses consistently. Additionally, the use of TypeScript provides better type safety and makes the code easier to read and understand.

export async function handleResponse<T>(
  promise: Promise<AxiosResponse<T>>,
  res: NextApiResponse,
  dataKey?: string // Optional argument to specify the data key
) {
  try {
    const response = await promise;
    // @ts-ignore Get the response data
    const responseData = dataKey ? response.data[dataKey] : response.data;
    return res.status(response.status).json(responseData);
  } catch (error: any) {
    return res.status(error.response?.status || 400).json({ message: error });
  }
}

import bigCommerceService from "@/services/bigCommerceService";
import { NextApiRequest, NextApiResponse } from "next";
import { handleResponse } from "@/lib/ApiHandler";
export default async function getAllProducts(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "GET")
    return res.status(405).json({ message: "Method not allowed." });

  const promise = bigCommerceService.get(
    `${process.env.BIGCOMMERCE_STORE_API_URL}/v3/catalog/products?include_fields=name,is_visible,custom_url,inventory_level,price&include=variants`
  );

  return handleResponse(promise, res, 'data');
}

Fetch Products in Typescript

Provide the rewritten ready-to-use files with TypeScript

// locations: pages/api/bigCOmmerce/orders/index.ts
import { handleResponse } from "@/lib/ApiHandler";
import { NextApiRequest, NextApiResponse } from "next";
import bigCommerceService from "@/services/bigCommerceService";

export default async function CreateOrder(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const response = bigCommerceService.post<Order.CreateOrderResponse>(
      `${process.env.BIGCOMMERCE_STORE_API_URL}/v2/orders`,
      req.body as Order.CreateOrderRequest
    );

    return handleResponse(response, res);
  } else {
    return res.status(400).json({ message: "Wrong Action" });
  }
}
// location: pages/api/bigCOmmerce/products/index.ts
import { ProductsBuilder } from "@/utils/getServerProducts";
import { NextApiRequest, NextApiResponse } from "next";

type FilterIds = Array<number>;

export default async function FetchProducts(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "GET") {
    return res.status(400).json({ message: "Wrong Action" });
  }

  const limit =
    typeof req.query?.limit === "string" ? req.query?.limit : undefined;
  const pick =
    typeof req.query?.pick === "string" ? req.query?.pick : undefined;

  try {
    const builder = new ProductsBuilder({ limit });
    const instance = (await builder.fromServer()).formatProducts();
    const filterIds: FilterIds =
      (req.query?.filter_ids as string)?.split(",").map(Number) || [];

    if (filterIds.length) {
      instance.filterProducts(filterIds);
    }

    if (req.query?.pick) {
      instance.randomlyPickProducts({ pick });
    }

    return res.status(200).json(instance.products);
  } catch (error: any) {
    return res.status(error.response?.status || 400).json({ message: error });
  }
}

Fetch all categories in typescript

import { headers } from "@/config/bigCommerce";

export default async function getAllCategories(query = "") {
  const response = await fetch(
    `${process.env.BIGCOMMERCE_STORE_API_URL}/v3/catalog/categories${
      query && "?" + query
    }`,
    {
      method: "GET",
      headers,
    }
  );

  const json = await response.json();
  let result = json.data;

  if (result) {
    result = result.filter(
      (val) =>
        val.is_visible && val.custom_url.url !== "/store/cnvs-analog-deskpad"
    );
  }

  return result;
}

The code beneath shows how to fetch all categories in TypeScript. It's important to note that beginners should have a basic understanding of TypeScript syntax and concepts before diving into this code. The code uses the fetch function to make a request to the specified API URL and retrieve the categories. The resulting data is then filtered to exclude any categories that are not visible or have a custom URL that matches a specific value. The TypeScript version of the code uses the get method from the bigCommerceService to perform the same request and filter the data. By using TypeScript, you can ensure type safety and catch errors early in your code.

import bigCommerceService from "@/services/bigCommerceService";

export default async function getAllCategories(query = "") {
  const { data: { data } }: { data: { data: Category.GetCategoryResponse[] }} = await bigCommerceService.get(
    `${process.env.BIGCOMMERCE_STORE_API_URL}/v3/catalog/categories${
      query && "?" + query
    }`
  );

  let result: Category.GetCategoryResponse[] = [];
  if (data) {
    result = data.filter(
      (val) =>
        val.is_visible && val.custom_url.url !== "/store/cnvs-analog-deskpad"
    );
  }

  return result;
}

Inside bigcommerce service

import HttpService from "./httpService";

HttpService.instance.defaults.headers.common["X-Auth-Token"] = 
  process.env.BIGCOMMERCE_TOKEN;
/* Verb */
const { get, post, put, delete: del } = HttpService.instance;

export default { get, post, put, delete: del };
© 2024 WOOTHINK. All Rights Reserved.
Site MapTerms and ConditionsPrivacy PolicyCookie Policy