Send file as response using NextJS API

Viewed 17019

As titled,

In NodeJs + Express, i can return a file as response with the following line

res.sendFile(absolute_path_to_the_file)

How can i achieve this with NextJs API, assuming if i want to return a single image from output folder inside NextJs directory? I can only see res.send() and res.json() as ways to return response, and im not sure how can i leverage it to return image as response back to the caller.

if i do like this

res.send(absolute_path_to_the_file)

It will just send me the string of the directory path. What i expect is the image send from the directory denoted by the directory path.

Need help here for this.

2 Answers

Answering my own question here for those who're curious to know too..

I made a thread about it in NextJS and they gave me a good answer to this - here

2 ways of doing this is either by using readStream

var filePath = path.join(__dirname, 'myfile.mp3');
var stat = fileSystem.statSync(filePath);

response.writeHead(200, {
    'Content-Type': 'audio/mpeg',
    'Content-Length': stat.size
});

var readStream = fileSystem.createReadStream(filePath);
// We replaced all the event handlers with a simple call to readStream.pipe()
readStream.pipe(response);

or change the object into buffer and use send method

/*
Project structure:
.
├── images_folder
│   └── next.jpg
├── package.json
├── pages
│   ├── api
│   │   └── image.js
│   └── index.js
├── README.md
└── yarn.lock
*/

// pages/api/image.js

import fs from 'fs'
import path from 'path'

const filePath = path.resolve('.', 'images_folder/next.jpg')
const imageBuffer = fs.readFileSync(filePath)

export default function(req, res) {
  res.setHeader('Content-Type', 'image/jpg')
  res.send(imageBuffer)
}

Both answers work in my case. Use process.cwd() to navigate to the files / image that needed to be send as response.

Here is a real-world example, much more advanced, with Sentry for debugging, and returning a stream with dynamic CSV filename. (and TypeScript types)

It's probably not as helpful as the other answer (due to its complexity) but it might be interesting to have a more complete real-world example.

Note that I'm not familiar with streams, I'm not 100% what I'm doing is the most efficient way to go, but it does work.

src/pages/api/webhooks/downloadCSV.ts

import { logEvent } from '@/modules/core/amplitude/amplitudeServerClient';
import {
  AMPLITUDE_API_ENDPOINTS,
  AMPLITUDE_EVENTS,
} from '@/modules/core/amplitude/events';
import { createLogger } from '@/modules/core/logging/logger';
import { ALERT_TYPES } from '@/modules/core/sentry/config';
import { configureReq } from '@/modules/core/sentry/server';
import { flushSafe } from '@/modules/core/sentry/universal';
import * as Sentry from '@sentry/node';
import {
  NextApiRequest,
  NextApiResponse,
} from 'next';
import stream, { Readable } from 'stream';
import { promisify } from 'util';

const fileLabel = 'api/webhooks/downloadCSV';
const logger = createLogger({
  fileLabel,
});

const pipeline = promisify(stream.pipeline);

type EndpointRequestQuery = {
  /**
   * Comma-separated CSV string.
   *
   * Will be converted into an in-memory stream and sent back to the browser so it can be downloaded as an actual CSV file.
   */
  csvAsString: string;

  /**
   * Name of the file to be downloaded.
   *
   * @example john-doe.csv
   */
  downloadAs: string;
};

type EndpointRequest = NextApiRequest & {
  query: EndpointRequestQuery;
};

/**
 * Reads a CSV string and returns it as a CSV file that can be downloaded.
 *
 * @param req
 * @param res
 *
 * @method GET
 *
 * @example https://753f-80-215-115-17.ngrok.io/api/webhooks/downloadCSV?downloadAs=bulk-orders-for-student-ambroise-dhenain-27.csv&csvAsString=beneficiary_name%2Ciban%2Camount%2Ccurrency%2Creference%0AAmbroise%20Dhenain%2CFR76%204061%208802%208600%200404%208805%20373%2C400%2CEUR%2CBooster%20Unly%20%20septembre%0AAmbroise%20Dhenain%2CFR76%204061%208802%208600%200404%208805%20373%2C400%2CEUR%2CBooster%20Unly%20%20octobre%0AAmbroise%20Dhenain%2CFR76%204061%208802%208600%200404%208805%20373%2C400%2CEUR%2CBooster%20Unly%20%20novembre%0A
 */
export const downloadCSV = async (req: EndpointRequest, res: NextApiResponse): Promise<void> => {
  try {
    configureReq(req, { fileLabel });
    const {
      csvAsString,
      downloadAs = 'data.csv',
    } = req?.query as EndpointRequestQuery;

    await logEvent(AMPLITUDE_EVENTS.API_INVOKED, null, {
      apiEndpoint: AMPLITUDE_API_ENDPOINTS.WEBHOOK_DOWNLOAD_CSV,
    });

    Sentry.withScope((scope): void => {
      scope.setTag('alertType', ALERT_TYPES.WEBHOOK_DOWNLOAD_CSV);

      Sentry.captureEvent({
        message: `[downloadCSV] Received webhook callback.`,
        level: Sentry.Severity.Log,
      });
    });

    await flushSafe();
    res.setHeader('Content-Type', 'application/csv');
    res.setHeader('Content-Disposition', `attachment; filename=${downloadAs}`);

    res.status(200);
    await pipeline(Readable.from(new Buffer(csvAsString)), res);
  } catch (e) {
    Sentry.captureException(e);
    logger.error(e.message);

    await flushSafe();

    res.status(500);
    res.end();
  }
};

export default downloadCSV;

Code is based on the Next Right Now boilerplate, if you want to dive-in into the configuration (Sentry, etc.): https://github.com/UnlyEd/next-right-now/blob/v2-mst-aptd-at-lcz-sty/src/pages/api/webhooks/deploymentCompleted.ts

Related