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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.