SvelteKit Filesystem Read Feature

| Published: January 20, 2024

| Updated: January 20, 2024

Rich Harris just announced a new feature to SvelteKit which will provide an improved approach to returning assets from your filesystem. The API is dead simple and it also means handling dynamic assets can now be included in the build process.

Dealing with the node fs is never a good time.

In the official docs, the explanation of the new read function is limited to one line:

Read the contents of an imported asset from the filesystem

import { read } from '$app/server';

function read(asset: string): Response;

At a glance, it might not mean a whole lot but this article will outline why this is a brilliant improvement, and give examples where you can include it in your project.

Skip to the FAQ’s

The example shown in this article is based on the Rich Harris fowl-play showcase project. He also walks through some of the steps in this video:

Before the update

Dealing with the node filesystem (fs) has always been a bane for many node.js developers. The unnecessary complexity and unexpected behaviors have lead Rich and the Svelte team to work on a better way to return files in SvelteKit.

Before the new read feature in SvelteKit, you would typically use fs.readFileSync() to return files. But the problem with that is you’re buffering the file to memory before you can do anything with it. And when you deal with large files, this becomes a problem.

Alternatively, you could stream the file using fs.createReadStream() like this:

reader = fs.createReadStream('input.txt');

reader.on('data', function (chunk) {

But this has hairs on it and it’s more complex than necessary.

If you wanted to have dynamic assets in your app, it was usually easier to set up a storage bucket, store the URL’s and just present those to the user. But if this wasn’t an option, you would have to find a fairly verbose solution that would work with the node fs system.


In order to simply simulate this dynamic file problem, we’ll use an example from the fowl-play project by Rich Harris, the creator of this feature.

In this showcase project, the app is a single page with a single img tag:

<img src="/duckduckgoose.jpg" alt="Either a duck, a duck or a goose" />

The img references the directory: src/routes/duckduckgoose.jpg (which is a folder - despite the .jpg extension), which contains 2 images (duck.jpg, and goose.jpg), and a +server.js file.

The +server.js file runs a simple randomness function, and either returns the duck.jpg file, or the goose.jpg file from the filesystem using fs.readFileSync().

import fs from 'node:fs';

export function GET() {
	const bird = Math.random() < 2 / 3 ? 'duck' : 'goose';
	const data = fs.readFileSync(`src/routes/duckduckgoose.jpg/${bird}.jpg`);

	return new Response(data, {
		headers: {
			'Content-Type': 'image/jpeg',
			'Content-Length': data.length.toString()

When the img tag on the page references src="/duckduckgoose.jpg", it’s not exactly referencing any specific image - but rather this GET endpoint in the +server.js file above. So when the application goes through the build process, neither the duck.jpg file nor the goose.jpg file will be included.


So this is to say, unless something is directly referencing a file in the application, it won’t be included in the build stage, and therefore your deployed app won’t be able to read the files you think are there. You’ll get an error because the img tag doesn’t point to anything - the directory doesn’t exist.

Note: Due to the lack of optimizations in the dev environment, this will work on your localhost. It just doesn’t work in build versions, i.e. deployed versions.

With the new update

The SvelteKit team have managed to fix this issue, and the new read function makes it :

import duck from './duck.jpg';
import goose from './goose.jpg';
import { read } from '$app/server';

export function GET() {
	const bird = Math.random() < 2 / 3 ? duck : goose;
	return read(bird);

And now, using the same randomness function, the app now works because the two duck.jpg and goose.jpg now form part of the application, in the build process.

This will work in all node environments - both developer and build.


Why this is great


  1. You won’t have to buffer the file to memory before sending in to the client,
  2. You can access dynamic imports,
  3. It’s simple, and you don’t have to deal with the seemingly infinite amount of fs read alternatives (each with its own shortcoming).

There’s a few reasons I ❤️ this implementation.

Firstly, dealing with the filesystem is a pain. There always seems to be quirks that take unnecessary amounts of time to fix.

In the first example where we used fs.readFileSync(), you’re buffering the image data into memory before the response. If you’re dealing with large files, this starts to become a problem, and your computation costs are going to take a hit.

Secondly, With the read API, you’re dealing with a gorgeous interface any lay can grasp.

return read(file);

I’m stoked for us SvelteKit developers that don’t have to go hell and back trying to deal with the filesystem anymore. 🎉


But what about those explicit imports?

Say you have a system which is dynamic and you can’t import the files explicitly like the above example.

We can remove those imports entirely, and use a blanket import to get every .jpg in the parent directory:

import { read } from '$app/server';

const fowl = import.meta.glob('./*.jpg', {
	as: 'url',
	eager: true
}); // Import all files in this folder ending in .jpg

const assets = Object.values(fowl); // Array of image URLs

export function GET() {
	const asset = assets[Math.floor(Math.random() * assets.length)]; // Pick a random image from the array
	return read(asset); // SEND IT 😎

With SvelteKit/Vite, this still works. If the file is in the directory, then it gets included in your build process!


The overall takeaway is that handling filesystem assets in SvelteKit just got a whole lot simpler. Maybe you don’t have to offload your asset system in your app to a cloud storage bucket.

Perhaps you’ve been dealing with a long-standing bug and you couldn’t figure out how to best dynamically import files into your deployed build.

With the new read feature, you get:

  1. Dynamic imports included in your build
  2. Cleaner code,
  3. Cost savings on not having to buffer files to array before sending them.
  4. No footguns

As I mentioned in the SvelteKit V2 article, the core Svelte team are constantly push new features that make developing with it so much better. This is just one of many awesome things to come.


Thanks for reading ❤️

Here are some other articles I think you might like!