Skip to main content
Skip to main content

How to Create a File Service

In this document, you’ll learn how to create a file service in the Medusa backend and the methods you must implement in it.

Overview

A file service class is defined in a TypeScript or JavaScript file that’s created in the src/services directory. The class must extend the AbstractFileService class imported from the @medusajs/medusa package.

Based on services’ naming conventions, the file’s name should be the slug version of the file service’s name without service, and the class’s name should be the pascal case of the file service’s name following by Service.

For example, create the file src/services/local-file.ts with the following content:

src/services/local-file.ts
import { AbstractFileService } from "@medusajs/medusa"
import {
DeleteFileType,
FileServiceGetUploadStreamResult,
FileServiceUploadResult,
GetUploadedFileType,
UploadStreamDescriptorType,
} from "@medusajs/types"

class LocalFileService extends AbstractFileService {
async upload(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult> {
throw new Error("Method not implemented.")
}
async uploadProtected(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult> {
throw new Error("Method not implemented.")
}
async delete(fileData: DeleteFileType): Promise<void> {
throw new Error("Method not implemented.")
}
async getUploadStreamDescriptor(
fileData: UploadStreamDescriptorType
): Promise<FileServiceGetUploadStreamResult> {
throw new Error("Method not implemented.")
}
async getDownloadStream(
fileData: GetUploadedFileType
): Promise<NodeJS.ReadableStream> {
throw new Error("Method not implemented.")
}
async getPresignedDownloadUrl(
fileData: GetUploadedFileType
): Promise<string> {
throw new Error("Method not implemented.")
}
}

export default LocalFileService
Multer Typing

The examples implement a file service supporting local uploads.

If you’re using TypeScript and you're following along with the implementation, you should install the Multer types package in the root of your Medusa backend to resolve errors within your file service types:

npm install @types/multer

constructor

You can use the constructor of your file service to access the different services in Medusa through dependency injection.

You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party provider’s APIs, you can initialize it in the constructor and use it in other methods in the service.

Additionally, if you’re creating your file service as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin, you can access them in the constructor.

Example

// ...
import { Logger } from "@medusajs/medusa"
import * as fs from "fs"

class LocalFileService extends AbstractFileService {
// can also be replaced by an environment variable
// or a plugin option
protected serverUrl = "http://localhost:9000"
protected publicPath = "uploads"
protected protectedPath = "protected-uploads"
protected logger_: Logger

constructor({ logger }: InjectedDependencies) {
// @ts-ignore
super(...arguments)
this.logger_ = logger

// for public uploads
if (!fs.existsSync(this.publicPath)) {
fs.mkdirSync(this.publicPath)
}

// for protected uploads
if (!fs.existsSync(this.protectedPath)) {
fs.mkdirSync(this.protectedPath)
}
}
// ...
}

Parameters

containerRecord<string, unknown>Required
An instance of MedusaContainer that allows you to access other resources, such as services, in your Medusa backend.
configRecord<string, unknown>
If this file service is created in a plugin, the plugin's options are passed in this parameter.

Methods

upload

This method is used to upload a file to the Medusa backend.

Example

class LocalFileService extends AbstractFileService {
// ...
async upload(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult> {
const filePath =
`${this.publicPath}/${fileData.originalname}`
fs.copyFileSync(fileData.path, filePath)
return {
url: `${this.serverUrl}/${filePath}`,
key: filePath,
}
}
// ...
}
Tip

This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.

Parameters

fileDataFileRequired
A multer file object. The file is uploaded to a temporary directory by default. Among the file’s details, you can access the file’s path in the path property of the file object.

Returns

PromisePromise<FileServiceUploadResult>Required
The details of the upload's result.

uploadProtected

This method is used to upload a file to the Medusa backend, but to a protected storage. Typically, this would be used to store files that shouldn’t be accessible by using the file’s URL or should only be accessible by authenticated users. For example, exported or imported CSV files.

Example

class LocalFileService extends AbstractFileService {
// ...
async uploadProtected(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult> {
const filePath =
`${this.protectedPath}/${fileData.originalname}`
fs.copyFileSync(fileData.path, filePath)
return {
url: `${this.serverUrl}/${filePath}`,
key: filePath
}
}
// ...
}
Tip

This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.

Parameters

fileDataFileRequired
A multer file object. The file is uploaded to a temporary directory by default. Among the file’s details, you can access the file’s path in the path property of the file object.

Returns

PromisePromise<FileServiceUploadResult>Required
The details of the upload's result.

delete

This method is used to delete a file from storage.

Example

class LocalFileService extends AbstractFileService {

async delete(
fileData: DeleteFileType
): Promise<void> {
fs.rmSync(fileData.fileKey)
}

// ...
}

Parameters

fileDataDeleteFileTypeRequired
The details of the file to remove.

Returns

PromisePromise<void>Required
Resolves when the file is deleted successfully.

getUploadStreamDescriptor

This method is used to retrieve a write stream to be used to upload a file.

Example

// ...
import { Stream } from "stream"

class LocalFileService extends AbstractFileService {
// ...
async getUploadStreamDescriptor({
name,
ext,
isPrivate = true,
}: UploadStreamDescriptorType
): Promise<FileServiceGetUploadStreamResult> {
const filePath = `${isPrivate ?
this.publicPath : this.protectedPath
}/${name}.${ext}`

const pass = new Stream.PassThrough()
const writeStream = fs.createWriteStream(filePath)

pass.pipe(writeStream)

return {
writeStream: pass,
promise: Promise.resolve(),
url: `${this.serverUrl}/${filePath}`,
fileKey: filePath,
}
}
// ...
}

Parameters

The details of the file being uploaded.

Returns

PromisePromise<FileServiceGetUploadStreamResult>Required
The result of the file-stream upload.

getDownloadStream

This method is used to retrieve a read stream for a file, which can then be used to download the file.

Example

class LocalFileService extends AbstractFileService {

async getDownloadStream({
fileKey,
isPrivate = true,
}: GetUploadedFileType
): Promise<NodeJS.ReadableStream> {
const filePath = `${isPrivate ?
this.publicPath : this.protectedPath
}/${fileKey}`
const readStream = fs.createReadStream(filePath)

return readStream
}

// ...
}

Parameters

fileDataGetUploadedFileTypeRequired
The details of the file.

Returns

PromisePromise<ReadableStream>Required
The read stream to read and download the file.

getPresignedDownloadUrl

This method is used to retrieve a download URL of the file. For some file services, such as S3, a presigned URL indicates a temporary URL to get access to a file.

If your file service doesn’t perform or offer a similar functionality, you can just return the URL to download the file.

Example

class LocalFileService extends AbstractFileService {

async getPresignedDownloadUrl({
fileKey,
isPrivate = true,
}: GetUploadedFileType
): Promise<string> {
// Local upload doesn't provide
// support for presigned URLs,
// so just return the file's URL.

const filePath = `${isPrivate ?
this.publicPath : this.protectedPath
}/${fileKey}`
return `${this.serverUrl}/${filePath}`
}

// ...
}

Parameters

fileDataGetUploadedFileTypeRequired
The details of the file.

Returns

PromisePromise<string>Required
The presigned URL to download the file

Test Implementation

Note

If you created your file service in a plugin, refer to this guide on how to test plugins.

After finishing your file service implementation:

1. Run the build command in the root of your Medusa backend:

npm run build

2. Start the backend with the develop command:

npx medusa develop

3. Upload a file using the Admin REST APIs or using the Medusa admin, for example, to upload a product's thumbnail.

Was this section helpful?