API with NestJS #54. Storing files inside a PostgreSQL database

JavaScript NestJS SQL TypeScript

This entry is part 54 of 168 in the API with NestJS

In the previous parts of this series, we’ve learned how to upload files to Amazon S3 and store their metadata in our PostgreSQL database. While this is a highly scalable approach, it might be overkill for our application. A great example is storing users’ avatars. They are usually small and simple files, and we might want to avoid additional costs of using services such as Amazon S3. To deal with the above case, we can use PostgreSQL.

Storing binary data in PostgreSQL

One of the ways to store binary data in PostgreSQL is with the bytea column. Due to how it works under the hood, it is appropriate for storing raw data as binary strings.

The SQL standard defines another data type called BLOB. While it works differently, it functions in a very similar manner.

Using the bytea column with TypeORM

Fortunately, the bytea data type is very straightforward to use with TypeORM. Let’s create a new entity to store our data:

databaseFile.entity.ts

Uint8Array is very similar to Buffer. If you want to know more about it, check out Node.js TypeScript #3. Explaining the Buffer

Let’s also set up a one-to-one relationship between the user and the above file to store avatars.

user.entity.ts

Thanks to creating the separate property, we can get the id of the file even without joining the DatabaseFile table. This neat trick can increase our performance a bit and avoid fetching the binary data unnecessarily.

If you want to know more about relationships, check out API with NestJS #7. Creating relationships with Postgres and TypeORM

We also need to add the appropriate method in the :

users.service.ts

Saving the files into the database

Above, we use the to create rows in our table dedicated to storing files. Let’s make the basics of it:

databaseFiles.service.ts

The crucial part above is that we require the user to provide a buffer. We can make it possible by following the NestJS documentation and using that utilizes the multer library under the hood.

users.controller.ts

To provide us with files, the user needs to perform a request with multipart/form-data. We go in-depth explaining how it works in Node.js TypeScript #6. Sending HTTP requests, understanding multipart/form-data

Retrieving the image

The last step in implementing the basics of managing avatars is a way to retrieve them.

Above, we can see that we expose just the id of the avatar. We now need to create a route that allows our frontend application to fetch the avatar. There are a few ways to implement that. The most straightforward approach would be to use the fact that the object is a writeable stream.

databaseFiles.controller.ts

If you want to know more about writeable streams, check out Node.js TypeScript #5. Writable streams, pipes, and the process streams

The above approach has a downside, though. When we use the decorator in the above way and pipe the stream manually, we strip ourselves of some of the features provided by NestJS. Fortunately, NestJS tried to address that by introducing StreamableFile.

databaseFiles.controller.ts

By default, the above results in setting the header to . Because of that, the browser doesn’t recognize it as an image.

databaseFiles.controller.ts

Above, we still inject the object, but with the option. Thanks to that, we still rely on NestJS to parse the return of the method and return it to the user.

We also set to so that the browser can interpret it correctly. Thanks to setting the header, the browser can also recognize the filename of our avatar. For example, we can notice that when the user attempts to save the file to the hard drive.

Above, we can see that the browser automatically suggested the file’s name to the user when saving.

Deleting files with transactions

A significant advantage of storing files in the SQL database is that we can manage them within a database transaction. This might come in handy when the user wants to replace an already existing avatar. When this happens, we need to delete the old file first and then upload a new one. Thanks to transactions, we can revert the whole operation when uploading the new avatar fails for some reason.

If you want to know more about transactions, check out API with NestJS #15. Defining transactions with PostgreSQL and TypeORM

The first step in achieving that is allowing the methods in to run with a query manager:

databaseFiles.service.ts

When we do the above, we can pass the query manager to the above methods from other services:

users.service.ts

Summary

In this article, we’ve learned a simple way of uploading files to PostgreSQL. While it might not be the most scalable approach, it might prove helpful in situations that could use a simple solution. Besides that, it has some advantages, such as the possibility of implementing transactions and easy backups. Thanks to that, it might prove to be valid, and it is worth knowing.

Series Navigation<< API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORMAPI with NestJS #55. Uploading files to the server >>
Subscribe
Notify of
guest
5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Nestor
Nestor
2 years ago

Hello I love this series … it is the best thing that I read … I wanted to consult you … I could not make my registration in aws, my sinternational credit card from a Panamanian bank rejected me … do you have any idea of why? in my bank they say that everything is correct … and I buy any product with my card in amazon. I don’t know what I’m doing wrong

catato.developer
catato.developer
2 years ago

I’m getting undefined when accessing file.buffer. Is there any workaround?

UPD:
I turned out that the problem was in my MulterModule initialization. I had specified dest option and that what made buffer be undefined. After I removed it everything became fine.

Last edited 2 years ago by catato.developer
maiky
maiky
1 year ago

Good content

Kevin
Kevin
1 year ago

Hello, Thank you so much for this post, it’s great! But I am having a write after end error from using stream.pipe() in retrieving. I wonder do you have any idea why that might be? Thanks!