40

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
  • 2
    aw it's not answered... I'm stuck at the same thing,, You got any luck? Commented Jul 25, 2020 at 21:03
  • 2
    @rakeshshrestha i asked the same question at Vercel Github and they responded me with this - github.com/vercel/next.js/discussions/… .. havent tested yet but answer given looks good Commented Aug 6, 2020 at 2:43

5 Answers 5

37

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.

Sign up to request clarification or add additional context in comments.

2 Comments

You can set Content-disposition header if you want change downloaded file name res.setHeader('Content-disposition', 'attachment; filename=filename.ext');
readStream.pipe(response) makes my code get stuck. I found how to fix it on the link you mentioned. Just replace with await new Promise(function (resolve) { readStream.pipe(response); readStream.on("end", resolve); });
32

As per NextJS 13, when using the App Router, you can do in the following way:

// before
res.status(200).setHeader('content-type', 'image/png').send(data);

// after
return new Response(data, { headers: { 'content-type': 'image/png' } });
// or
const response = new NextResponse(data)
response.headers.set('content-type', 'image/png');
return response;

Note: data can be Blob/Stream/Buffer.

For more info, refer to this GitHub Issue.

2 Comments

The reasoning here will be hard to track down in the future. Your GitHub issue links to a Reddit post. Both exterior links that could die at any time. Please consider adding the most important information from there to your answer.
this worked for me: return new NextResponse(data, { status: 200, headers: new Headers({ "content-disposition": `attachment; filename=test.pdf`, "content-type": "application/pdf", "content-length": data.size + "", }) })
3

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

Comments

3

In addition to Fred A's answer, in case you are having your code getting stuck, it's the last line. I found out how to fix this issue through the link from his answer. I wrote a reply, but I'm putting the full answer here for better visibility.

import fs from "fs";

export default function(req, res) {
  const filePath = path.join(__dirname, 'myfile.mp3');

  const { size } = fs.statSync(filePath);

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

  const readStream = fs.createReadStream(filePath);

  await new Promise(function (resolve) {
    readStream.pipe(res);

    readStream.on("end", resolve);
  });
});

Comments

1

assuming you have the file in byteArray form, then you do this inside your Next.js API to send file to browser:

res.setHeader('Content-Disposition', 'attachment; filename="preferred-filename.file-extension"');
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Length', byteArray.length);
res.status(200).send(Buffer.from(byteArray));

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.