How to Return a Playwright Screenshot from an Express API
Capture a page with Playwright and return its PNG bytes from an Express route, with correct headers, error handling, and deployment guidance.
Call Playwright’s page.screenshot(), then send the returned bytes as an Express response with a matching image content type. For PNG, res.type('png').send(buffer) returns the screenshot directly without writing a temporary file.
The example below assumes your application has already created a Playwright page. Browser and page creation, sharing, and cleanup depend on your application architecture; choose a lifecycle that fits your deployment and confirm the APIs against your installed Playwright and Express versions. The linked documentation describes the relevant APIs: Playwright Page and Express Response.
1. Return screenshot bytes from an Express route
import express from 'express';
const app = express();
// Assumption: page has already been created and is available here.
app.get('/screenshot', async (req, res, next) => {
try {
const image = await page.screenshot({ type: 'png' });
res.type('png').send(image);
} catch (error) {
next(error);
}
});
app.listen(3000);
page.screenshot() produces the image data. Express can send a Buffer using res.send(); explicitly set the content type because a Buffer otherwise defaults to application/octet-stream. res.type('png') sets image/png. The response method also ends the request. Sources: Playwright Page API, Express Response API, and Express routing guide.
This is a documentation-based pattern, not a tested application. Ensure that page is valid when the route runs and that your installed package versions support the options you use.
2. Make the endpoint useful and safe
Choose the screenshot type and response header together
For PNG, use { type: 'png' } and res.type('png'). If you change the encoding, set the corresponding response type too: JPEG bytes should be sent as image/jpeg, and WebP bytes as image/webp. Do not label one format as another; clients use the content type to interpret the response.
// PNG
const image = await page.screenshot({ type: 'png' });
res.type('png').send(image);
// JPEG
const jpeg = await page.screenshot({ type: 'jpeg', quality: 80 });
res.type('jpeg').send(jpeg);
Playwright documents screenshot options such as type, quality for JPEG, and path. Check the Page API reference for the options available in your installed version. Match the Express content type to the chosen encoding.
Handle failures through Express
Passing the error to next(error) delegates it to Express error handling. Your application should have an error handler that logs appropriately and sends a response when headers have not yet been sent. If an error happens after response output has begun, the connection may need to be closed; Express’s error handling guide describes this case. Avoid trying to send a second response after the first one has started.
app.use((error, req, res, next) => {
if (res.headersSent) {
return next(error);
}
console.error(error);
res.status(500).json({ error: 'Screenshot capture failed' });
});
Keep internal exception details out of public responses. The example logs the error server-side and returns a generic message; adapt logging and error policy to your service.
Decide where the browser and page live
The route example receives an existing page; it does not prescribe whether your app creates a new browser for each request, reuses a browser, or uses a worker pool. Those are application and deployment decisions. Whichever lifecycle you choose, make sure concurrent requests do not accidentally operate on the same page state, and close pages and browsers according to that lifecycle when they are no longer needed. The cited API references do not define a universal deployment setup.
3. Choose in-memory bytes or a saved file
| Approach | Use it when | Consider |
|---|---|---|
Buffer with res.send() |
The endpoint should capture and immediately return an image. | No intermediate file is needed. Set the content type explicitly. |
Save with Playwright path, then res.sendFile() |
Your workflow needs a filesystem artifact or file-oriented transfer. | Construct and constrain paths, consider cleanup, and account for concurrent requests. |
For a file workflow, Playwright’s screenshot path option writes the image to the server filesystem. Express res.sendFile() transfers a file; it is not an in-memory screenshot response. If you use it, do not let an untrusted request choose an arbitrary path. Use a controlled directory and, where appropriate, Express’s root option to constrain relative paths. See Express’s sendFile documentation.
import path from 'node:path';
const outputPath = path.join(approvedScreenshotDirectory, 'capture.png');
await page.screenshot({ path: outputPath, type: 'png' });
res.type('png').sendFile(outputPath, (error) => {
if (error) next(error);
});
This abbreviated example assumes approvedScreenshotDirectory is a server-controlled location and that your app manages file naming, cleanup, and simultaneous requests. Unique filenames or per-request directories can avoid collisions.
4. Call the endpoint from clients
cURL
curl --fail --show-error http://localhost:3000/screenshot -o screenshot.png
The output filename is local to the client. The endpoint response should carry Content-Type: image/png.
Python
import requests
response = requests.get('http://localhost:3000/screenshot', timeout=90)
response.raise_for_status()
with open('screenshot.png', 'wb') as image_file:
image_file.write(response.content)
print(response.headers.get('content-type'))
Node.js
const response = await fetch('http://localhost:3000/screenshot');
if (!response.ok) {
throw new Error(`Screenshot request failed: ${response.status}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('screenshot.png', bytes)
);
console.log(response.headers.get('content-type'));
5. Operational considerations
Performance and response size
A screenshot request includes page rendering and image transfer. The dossier provides no benchmark for capture time or image size, so measure those in your own target environment. Full-page captures and high-resolution images can produce larger responses than viewport captures. Set limits appropriate to your endpoint and client, and avoid holding unnecessary screenshot buffers or pages longer than needed.
Reliability
- Ensure the route always completes with a response or passes an error to Express; an unfinished handler can leave the client waiting.
- Check that the page is ready for the state you want to capture before calling
screenshot(). Choose any waiting behavior based on the target page and installed Playwright API. - Plan browser and page cleanup around your chosen application lifecycle.
- Account for client disconnects and errors while sending a file or buffer in your service’s normal error policy.
Cost and deployment
This approach runs browser capture inside your own application environment. The research sources do not specify hosting requirements, resource consumption, or costs; those depend on your runtime, concurrency, target pages, and infrastructure. Confirm that your deployment supports the browser process and dependencies you have selected, then observe resource use under the workload you expect.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The downloaded file is not recognized as an image. | The response has the default application/octet-stream type or a type that does not match the bytes. |
Set res.type('png') (or the correct format) before res.send(). |
| The request hangs. | The route did not send or end a response, or capture has not completed. | Ensure success sends the image and failures reach error handling. Review the route and capture lifecycle. |
| The route returns an error instead of an image. | The screenshot operation failed, or the assumed page is unavailable or closed. |
Log the server-side error, verify page creation and lifecycle, and pass failures to Express error handling. |
| Express reports that headers were already sent. | The handler attempted another response after response output began. | Check res.headersSent in error handling and delegate to the next error handler when true. |
| A saved-file route returns the wrong or another request’s image. | Requests may be sharing or overwriting a path. | Use controlled unique paths per capture and define cleanup. Keep paths server-controlled or constrained with root. |
| The client saves an error payload with a PNG filename. | The client did not check the HTTP status before writing the response body. | Check the status and raise or handle non-success responses before saving bytes. |
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its API returns a screenshot from one GET request, and its options include image formats, PDF, full-page capture, selectors, viewport and device presets, custom CSS and JavaScript, waits, request blocking, cookies and headers, and caching. See the ScreenshotNeo API documentation for parameters and usage.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; responses include page-verdict and billing headers.
- An MCP server provides screenshot tools for AI agents, including Claude, Cursor, and other MCP clients.
- The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
8. FAQ
How do I send a Playwright screenshot in an Express response?
Await page.screenshot(), set the matching image type with res.type(), and pass the returned bytes to res.send().
Does Express return a Buffer as an image automatically?
It sends Buffer data, but the default content type is application/octet-stream. Set the image type explicitly.
Should I save the screenshot before returning it?
Only when your workflow needs a file artifact. For an immediate response, sending the screenshot bytes avoids creating a file solely for transfer.
Where should I create and close the Playwright page?
That depends on your application’s browser lifecycle and deployment. The route pattern assumes a usable page is already available; define ownership and cleanup in the surrounding application.


