Building APIs with TypeScript
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:
- Find the request and response payload
- Copy the response payload to a separate file and use a tool like QuickType to generate a TypeScript file with the response payload
- Insert the generated TypeScript code into the appropriate location and name it based on the file name
- Change the target JavaScript file to TypeScript and add the necessary infrastructure, input, and output types.
- 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.
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.
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 };