Skip to main content

Uploading Files

Vendure handles file uploads with the GraphQL multipart request specification. Internally, we use the graphql-upload package. Once uploaded, a file is known as an Asset. Assets are typically used for images, but can represent any kind of binary data such as PDF files or videos.

Upload clients

Here is a list of client implementations that will allow you to upload files using the spec. If you are using Apollo Client, then you should install the apollo-upload-client npm package.

For testing, it is even possible to use a plain curl request.

The createAssets mutation

The createAssets mutation in the Admin API is the only means of uploading files by default.

Here's an example of how a file upload would look using the apollo-upload-client package:

Tsx
import { gql, useMutation } from "@apollo/client";const MUTATION = gql`  mutation CreateAssets($input: [CreateAssetInput!]!) {    createAssets(input: $input) {      ... on Asset {        id        name        fileSize      }      ... on ErrorResult {        message      }    }  }`;function UploadFile() {    const [mutate] = useMutation(MUTATION);    function onChange(event) {        const {target} = event;        if (target.validity.valid) {            mutate({                variables: {                    input: Array.from(target.files).map((file) => ({file}));                }            });        }    }    return <input type="file" required onChange={onChange}/>;}

Custom upload mutations

How about if you want to implement a custom mutation for file uploads? Let's take an example where we want to allow customers to set an avatar image. To do this, we'll add a custom field to the Customer entity and then define a new mutation in the Shop API.

Configuration

Let's define a custom field to associate the avatar Asset with the Customer entity. To keep everything encapsulated, we'll do all of this in a plugin

src/plugins/customer-avatar/customer-avatar.plugin.ts
import { Asset, LanguageCode, PluginCommonModule, VendurePlugin } from '@vendure/core';@VendurePlugin({    imports: [PluginCommonModule],    configure: config => {        config.customFields.Customer.push({ // [!code highlight]            name: 'avatar', // [!code highlight]            type: 'relation', // [!code highlight]            label: [{languageCode: LanguageCode.en, value: 'Customer avatar'}], // [!code highlight]            entity: Asset, // [!code highlight]            nullable: true, // [!code highlight]        }); // [!code highlight]        return config;    },})export class CustomerAvatarPlugin {}

Schema definition

Next, we will define the schema for the mutation:

src/plugins/customer-avatar/api/api-extensions.ts
import gql from 'graphql-tag';export const shopApiExtensions = gql`extend type Mutation {  setCustomerAvatar(file: Upload!): Asset}`

Resolver

The resolver will make use of the built-in AssetService to handle the processing of the uploaded file into an Asset.

src/plugins/customer-avatar/api/customer-avatar.resolver.ts
import { Args, Mutation, Resolver } from '@nestjs/graphql';import { Asset } from '@vendure/common/lib/generated-types';import {    Allow, AssetService, Ctx, CustomerService, isGraphQlErrorResult,    Permission, RequestContext, Transaction} from '@vendure/core';@Resolver()export class CustomerAvatarResolver {    constructor(private assetService: AssetService, private customerService: CustomerService) {}    @Transaction()    @Mutation()    @Allow(Permission.Authenticated)    async setCustomerAvatar(        @Ctx() ctx: RequestContext,        @Args() args: { file: any },    ): Promise<Asset | undefined> {        const userId = ctx.activeUserId;        if (!userId) {            return;        }        const customer = await this.customerService.findOneByUserId(ctx, userId);        if (!customer) {            return;        }        // Create an Asset from the uploaded file        const asset = await this.assetService.create(ctx, {            file: args.file,            tags: ['avatar'],        });        // Check to make sure there was no error when        // creating the Asset        if (isGraphQlErrorResult(asset)) {            // MimeTypeError            throw asset;        }        // Asset created correctly, so assign it as the        // avatar of the current Customer        await this.customerService.update(ctx, {            id: customer.id,            customFields: {                avatarId: asset.id,            },        });        return asset;    }}

Complete Customer Avatar Plugin

Let's put all these parts together into the plugin:

Ts
import { Asset, PluginCommonModule, VendurePlugin } from '@vendure/core';import { shopApiExtensions } from './api/api-extensions';import { CustomerAvatarResolver } from './api/customer-avatar.resolver';@VendurePlugin({    imports: [PluginCommonModule],    shopApiExtensions: {        schema: shopApiExtensions,        resolvers: [CustomerAvatarResolver],    },    configuration: config => {        config.customFields.Customer.push({            name: 'avatar',            type: 'relation',            label: [{languageCode: LanguageCode.en, value: 'Customer avatar'}],            entity: Asset,            nullable: true,        });        return config;    },})export class CustomerAvatarPlugin {}

Uploading a Customer Avatar

In our storefront, we would then upload a Customer's avatar like this:

Tsx
import { gql, useMutation } from "@apollo/client";const MUTATION = gql`  mutation SetCustomerAvatar($file: Upload!) {    setCustomerAvatar(file: $file) {      id      name      fileSize    }  }`;function UploadAvatar() {  const [mutate] = useMutation(MUTATION);  function onChange(event) {    const { target } = event;      if (target.validity.valid && target.files.length === 1) {      mutate({         variables: {          file: target.files[0],        }        });    }  }  return <input type="file" required onChange={onChange} />;}
Was this chapter helpful?
Report Issue
Edited Feb 2, 2026·Edit this page