Back to all courses
Are you building RESTful APIs correctly? In this video, let's create CRUD endpoints to support a job board API using TypeScript, Node/Express.js, RapidAPI Client, and Xata. This is the second video in this series where we will add error handling and additional type safety to our Express.js CRUD endpoints using Xata as the database and the RapidAPI Client extension for testing.
I publish weekly videos about Web Development! I am constantly learning the latest and greatest in Web Development.
Let's create a RESTful API using Node.js, Express.js, Typescript, RapidAPI, and Xata for the database.
If you missed the first part of this article series, you can read its first part here.
In Part 1, we defined and configured our CRUD endpoints. In this article, we will add error handling and subsequently additional types to our endpoints in order to improve their quality and take advantage of all the TypeScript richness that we have.
RapidAPI and Xata are co-sponsors of this guide. The amazing feature about RapidAPI is not just that we can host our API there so that we can make money, but also that they provide an extension right inside of VS Code where we can test all of our CRUD endpoints for the Job's API we developed in part 1.
To follow along and test things as we go, you'll need to install the RapidAPI Client extension. On the other side, our database work is done in Xata as well.
However, Xata itself provides a free serverless database where we can quickly specify our data and columns before pulling that information into our application.
You can access the GitHub repository containing the code for the Node.js API with the RapidAPI TypeScript Xata library here.
Let's dive into the code. In the index.ts
file, we import the getXataClient
module from ./xata
. This is created by Xata, and it defines our client
as well as the types for our database-stored jobs.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();app.get('/api/jobs', async (req: Request, res: Response) => {const jobs = await xata.db.job.getAll();res.json(jobs);});app.post('/api/jobs', async (req: Request, res: Response) => {const job = req.body;const createdJob = await xata.db.job.create(job);res.json(createdJob);});app.put('/api/jobs/:id', async (req: Request, res: Response) => {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);res.json(updatedJob);});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);});app.listen(port, () => {console.log(`Server running at port ${port}`);});
You can see inside our endpoints that we're already utilizing the XataClient
to query all the information we're seeking. However, TypeScript lacks a little clarity and our error handling is absent.
What does error handling mean? How can we actually handle this problem if our code calls the .getAll()
database function and anything goes wrong? It is our job to handle this error graciously and communicate that back to the calling user of this API.
In order to handle these errors, we will employ a try-catch block. A try-catch block is utilized to alleviate coding errors and prevent program crashes during execution. It tries a block of code that might result in an error. If the error (exception) occurs, the program will execute an alternative piece of code as opposed to crashing.
You will have your try
followed by your catch (err)
with an error within it. Now I have a useful snippet within Visual Studio Code. I will relocate all of our data inside of the try-catch block. Therefore, if a mistake occurs, it will be detected and logged.
Now the challenge is, how do we respond to the user? We may do a res.status(500)
and we'll not delve any deeper into error messages. In this situation, however, we will simply return a status code of 500
to indicate that an error occurred, followed by an error message encoded as json(err: 'Something went wrong')
.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();app.get('/api/jobs', async (req: Request, res: Response) => {const jobs = await xata.db.job.getAll();res.json(jobs);});app.post('/api/jobs', async (req: Request, res: Response) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);res.json(createdJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id', async (req: Request, res: Response) => {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);res.json(updatedJob);});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);});app.listen(port, () => {console.log(`Server running at port ${port}`);});
There are an endless number of possible degrees of detail for error handling. In this instance, we are just able to explain that something went wrong without providing further detail. So let's put this to the test.
We may open the tab for RapidAPI Client extension. The POST is located in our project's directory.
I've previously produced something in the past. I'll tweak it slightly and call it "Cool Company 2." This should function as intended since it will generate the specified piece of data and then return it to us.
The newly created piece of data that you can see appears instantly in the database browser, where it may be viewed. What happens, though, if we attempt to simulate this call by throwing an error? So, if we just write throw new Error('AGHHHHHH')
, it will effectively bypass this catch
or try
.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();app.get('/api/jobs', async (req: Request, res: Response) => {const jobs = await xata.db.job.getAll();res.json(jobs);});app.post('/api/jobs', async (req: Request, res: Response) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);throw new Error('AGHHHHH');res.json(createdJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id', async (req: Request, res: Response) => {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);res.json(updatedJob);});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);});app.listen(port, () => {console.log(`Server running at port ${port}`);});
Let's go ahead and send this. We will see that an error message is returned. We examine the response headers and enter the Raw to determine that this is a "500 Internal Server Error".
In this case, we want to defend ourselves on each of these separate errors. Thus, we may proceed and include try-catch blocks in our endpoint snippets. In addition, we must add the response 500
to each endpoint snippet.
In my opinion, we can do a better job of articulating what we'll return. Therefore, in each of these API calls, we want to either return anything with an error message such as (err: "Something went wrong")
. However, I want to utilize Typescript for this.
My first notion was to implement a type myResponse
with a err
property that is a string
and a data
property that might initially be any
. In this example, though, we're performing a type, and in this situation, both err
and data
may be null. We may thus say null
or string
followed by null
or any
.
Now, we can describe what the response from our API endpoint will include. This may be accomplished by passing a type to the Response
function and specifying that the type is MyResponse
. In addition, we must return an object where the data
property is jobs
and where the err
property is null
.
Additionally, we must add data: null
to our err: 'Something went wrong'
message. Essentially, every request to this endpoint will verify whether or not an error message is returned in the response from the API endpoint. If not, they can presume the data
attribute can be used as a successful response.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();type MyResponse = {err: null | string;data: null | any;};app.get('/api/jobs', async (req: Request, res: Response<MyResponse>) => {try {const jobs = await xata.db.job.getAll();res.json({data: jobs, err: null});} catch (err) {console.error(err);res.status(500).json({data: null, err: 'Something went wrong'});}});app.post('/api/jobs', async (req: Request, res: Response) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);res.json(createdJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);res.json(updatedJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.listen(port, () => {console.log(`Server running at port ${port}`);});
This works perfectly, however having to define error
when you don't need it is a little tedious. Furthermore, defining data
when you don't require it in this context is a bit cumbersome as well.
We can modify this such that a union of two distinct types may be created. Therefore, we can state that it will either contain a err
property, which is of type string
, or a data
property, in which case we may eliminate null
. Getting rid of err:null
and data:null
is therefore a step in the right direction.
Currently, we are utilizing the data type any
. However, there is an extra step we may take. Anyone who discusses TypeScript will urge you not to use 'any' anywhere. The challenge, however, is that the response from these API endpoints will vary depending on the endpoint
Generics allow us to select the type of data that will be returned in a dynamic manner. Therefore, we may state that my answer will have a type of <T>
and then refer to that type in our code snippet. Therefore, when we define this answer, it will be an instance or appear as <MyResponse>.
Additionally, we must now specify what <T>
is and the type of my response, which in this case is referred to by the data
attribute. My response will thus be of the kind <MyResponse<Job[]>>
. This will indeed return a job
array, so it appears to be a suitable match. I would also like to be more specific with the return
function and ensure that we actually return
properly.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();type MyResponse<T> =| {err: string;}| {data: T;};app.get('/api/jobs', async (req: Request, res: Response<MyResponse<Job[]>>) => {try {const jobs = await xata.db.job.getAll();return res.status(200).json({data: jobs});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.post('/api/jobs', async (req: Request, res: Response) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);res.json(createdJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);res.json(updatedJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.listen(port, () => {console.log(`Server running at port ${port}`);});
Let's examine this in the context of the POST example. Accordingly, we can modify this to indicate that we will return a response
of type MyResponse
with the type Job
. Therefore, we wish to provide an object whose data
field is set to the createdJob
, which appears correct.
Another thing to note is that when we call create
, it will either return to us or return the object it generated. Here, we must additionally add return
and res.status(201)
, where 201 indicates it is created.
We can perform one further action. We can pass types and two empty objects within the request
, with the last object representing the request body. So, this is essentially defining what we'll get inside of the request body that comes to us.
Adding this type signifies that I am defining the body to be a job
. Now, if I hover over job
, it gives intelligence that it is a job type and can display its attributes such as company geography
, id
, job link
, and title
. Now we have TypeScript not just on our response, but also on the incoming body data that we will receive.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();type MyResponse<T> =| {err: string;}| {data: T;};app.get('/api/jobs', async (req: Request, res: Response<MyResponse<Job[]>>) => {try {const jobs = await xata.db.job.getAll();return res.status(200).json({data: jobs});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.post('/api/jobs',async (req: Request<{}, {}, Job>, res: Response<MyResponse<Job>>) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);return res.status(201).json({data: createdJob});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);res.json(updatedJob);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.listen(port, () => {console.log(`Server running at port ${port}`);});
This will be repeated in this step for the PUT call snippet. Therefore, we will define the job
type for our request here. Then there is req.params.id
, which does not exist on the empty string type or an empty object. This is because the parameters for the route are rather defined within this first object.
So we may create an id
followed by a string
. If I click on params
, I will obtain intelligence about id
. Here, we must additionally provide our type. Therefore, we will specify that the inbound route's params
will have this structure, with an id
attribute of type string
. This update function will now be aware that it has a unique id
and a job
type.
There is one thing we must consider: what happens if the update fails to locate the existing 'job'? We'll check to see if we don't get an updatedJob
response and then address the matter accordingly.
Consequently, if we do not receive an updatedJob
response, we will assume that it was never found. So we may call res.status˜ and return a
404status, followed by a
jsonresponse with the error message
err: 'Job not found'`
The only thing we have yet to do is define the Response
type. So this Response
type will be a type of MyResponse
, and this will include a type of Job
; we will then close both of these types. Observe that we have a little issue here, which can be corrected by adding the data
property and passing in the updatedJob
parameter.
Just like we did previously, we'll return and res.status(200)
. Notice how the length of these functions has increased as we begin to define our type in order to control what we import or what we get as parameters, return, and error handling.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();type MyResponse<T> =| {err: string;}| {data: T;};app.get('/api/jobs', async (req: Request, res: Response<MyResponse<Job[]>>) => {try {const jobs = await xata.db.job.getAll();return res.status(200).json({data: jobs});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.post('/api/jobs',async (req: Request<{}, {}, Job>, res: Response<MyResponse<Job>>) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);return res.status(201).json({data: createdJob});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id',async (req: Request<{id: string}, {}, Job>,res: Response<MyResponse<Job>>) => {try {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);if (!updatedJob) {return res.status(404).json({err: 'Job not found.'});}return res.status(200).json({data: updatedJob});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.delete('/api/jobs/:id', async (req: Request, res: Response) => {try {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);res.json(deletedRecord);} catch (err) {console.error(err);res.status(500).json({err: 'Something went wrong'});}});app.listen(port, () => {console.log(`Server running at port ${port}`);});
The same process will be done with the DELETE request. We want to return inside the type and with an 'id' and string
. Then, we may have an empty object, and since there is no body available, we can add another empty object inside of it.
Here, we will receive an id
attribute, which can be used to reference the object. Then, we'll return a myResponse
with a job
type. Instead of just returning the deletedRecord
, we will return an object whose data
property is the deletedRecord
. Here, we may have the same issue that 'deletedRecord' may not return. It may not return a deletedRecord
if it was unable to locate one with the id
we gave.
In this scenario, we may apply the if()
function once again, like we just did for the previous request. We will then return
a res.status(404)
, followed by json(err: 'job not found')
.
ts
import {getXataClient, Job} from './xata';import dotenv from 'dotenv';import express, {Express, Request, Response} from 'express';dotenv.conf();const app: Express = express();const port = process.env.PORT || 3000;const xata = getXataClient();type MyResponse<T> =| {err: string;}| {data: T;};app.get('/api/jobs', async (req: Request, res: Response<MyResponse<Job[]>>) => {try {const jobs = await xata.db.job.getAll();return res.status(200).json({data: jobs});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.post('/api/jobs',async (req: Request<{}, {}, Job>, res: Response<MyResponse<Job>>) => {try {const job = req.body;const createdJob = await xata.db.job.create(job);return res.status(201).json({data: createdJob});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.put('/api/jobs/:id',async (req: Request<{id: string}, {}, Job>,res: Response<MyResponse<Job>>) => {try {const id = req.params.id;const job = req.body;const updatedJob = await xata.db.joob.update(id, job);if (!updatedJob) {return res.status(404).json({err: 'Job not found.'});}return res.status(200).json({data: updatedJob});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.delete('/api/jobs/:id',async (req: Request<{id: string}, {}, {}>,res: Response<MyResponse<Job>>) => {try {const id = req.params.id;const deletedRecord = await xata.db.job.delete(id);if (!deletedRecord) {return res.status(404).json({err: 'Job not found.'});}return res.status(200).json({data: deletedRecord});} catch (err) {console.error(err);return res.status(500).json({err: 'Something went wrong'});}});app.listen(port, () => {console.log(`Server running at port ${port}`);});
Let's go back over to RapidAPI Client Extension, and test whether all endpoint function correctly. So let's run our GET; we should get a list of jobs back.
Now, let's do a POST to create another one and call it "Cool Company No. 3." We can proceed with sending this. If we examine this JSON string, we can see that we obtain our data.
Then, we'll navigate to Update Job. Furthermore, we will need to adjust the path prior to making the request, and then we will change the company's name to "Extra Cool Company." Let's tap Send and we'll get the response.
Now, if we return to Xata, we can see that this change has also been implemented there. Then we'll proceed with the DELETE request. Remember that we must replace this id. We will send the deletion request and should receive a response.
Now add some information to the Endpoints. Let's call it Delete Job By Id, and then in the Update Job, we'll execute Update Job By Id, followed by Create New Job, and Get All Jobs.
Syncing all of these endpoints in RapidAPI Studio is the last thing I want to do. The revised description should appear when we sync. Now that we've brought them in here, we can safely store them. These might be distributed to others so that they may do their own testing.
In a nutshell, this guide is intended to teach you how to add error handling and types. We started with TypeScript, then added error handling and some more types. We got some help with our types from the code provided by Xata, which we can then reference within our code.