Within the previous tutorial, you learned how to add routes to your application and handle requests with their related responses. When building web applications, the need to serve files arises quickly. And with that, you also need routes to respond requests with file resources. If you’re serving HTML views to your client, you might want to display images or provide JavaScript files to enhance the client side functionality.
This guide walks you through the setup of hapi to serve static files like images, JavaScript files, compressed archives (.zip
, .tar
, etc.).
Before diving into the details, have a look at the series outline and find posts that match your interests and needs.
hapi Series Overview
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- Route Handling and Drive Traffic to Your Server
- How to Serve Static Files (Images, JS, CSS, etc.)
- How to Use Query Parameters
- Optional Path Parameters
- Multi-Segment/Wildcard Path Parameters
- Ignore Trailing Slashes on Route Paths
- How to Fix “Unsupported Media Type”
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
Prepare Your Server
Hapi doesn’t ship with functionality to serve static files by default. That keeps the actual framework footprint small and you can add this feature easily by using the inert plugin. We also have tutorials that guide you through the extension hapi’s functionality using plugins and how to create your own custom hapi plugin.
Once you registered the inert plugin to your hapi server instance, it adds new handler
methods to serve files and directories and also decorates the reply
object with a file
method to serve file resources.
hapi v17
const Hapi = require('hapi')
// create new server instance
const server = new Hapi.Server({
host: 'localhost',
port: 3000
})
async function liftOff () {
await server.register({
plugin: require('inert')
})
await server.start()
console.log('Server started at: ' + server.info.uri)
}
liftOff()
hapi v16
var Hapi = require('hapi')
// create new server instance
var server = new Hapi.Server()
// add server’s connection information
server.connection({
host: 'localhost',
port: 3000
})
server.register({
register: require('inert')
}, function(err) {
if (err) throw err
server.start(function(err) {
console.log('Server started at: ' + server.info.uri)
})
})
The snippet above just illustrates how to prepare your hapi server to serve static files from disk. Within the following sections, you’ll learn how to send files as response and serve files from a directory.
Send Any File (Images, JS, etc.)
When using HTML views, you probably want to define specific CSS or JS files that can be requested from your server. You may consume some of your 3rd party dependencies from CDN’s and others are shipped with your project locally. If you want to serve a specific file via a given route, define the filename including file extension as a route path. Afterwards, you can utilize the reply.file()
method to serve the requested file from local disk.
In the following example, we’ve reduced the exemplary code snippet to just represent the route definition. The most interesting part is the handler
function that responds the request using the reply.file()
function and serves the mylocaljavascript.js
file from the given file location on your server’s disc.
hapi v17
server.route({
method: 'GET',
path: '/mylocaljavascript.js',
handler: (request, h) => {
// h.file() expects the file path as parameter
return h.file('../path/to/my/localjavascript.js')
}
})
hapi v16
server.route({
method: 'GET',
path: '/mylocaljavascript.js',
handler: function (request, reply) {
// reply.file() expects the file path as parameter
reply.file('../path/to/my/localjavascript.js')
}
})
As already mentioned earlier, the reply.file()
method expects the local file’s path as a parameter. Afterwards, hapi creates the response and returns the requested file (if found).
Serve Files From a Directory
With the help of inert, you can serve all files located in a selected directory. Using path parameters for a route definition, you’re able to just define a single route that serves a given type of requests. The following example defines a route that serves JS files for requests that come in at /js/<file>
.
Actually, the path parameter name doesn’t matter. Using the asterisk extension does not restrict the file depth and allows you to make use of files located in subfolders. In case you only want to support root level files,remove the asterisk from the path.
server.route({
method: 'GET',
path: '/js/{file*}',
handler: {
directory: {
path: 'public/js'
}
}
})
The directory handler simplifies the route configuration and you just need to provide the correct path to the local JS files on your server’s disk. Once your server receives a request to serve a specific JS file, inert looks up the defined path to find the requested file. If available, the file gets served as a response for the current request. If the file isn’t available at the specified location, your server responds with a 404 Not Found
.
By default, inert doesn’t list the directory contents. If a file wasn’t found, you’ll receive a 404
. Using the listing: true
option besides the path
property for a directory
handler, you can define that any request to /js
is able to list the contents of /public/js
directory.
server.route({
method: 'GET',
path: '/js/{file*}',
handler: {
directory: {
path: 'public/js',
listing: true
}
}
})
The route snippet above allows users list the contents of your public/js
folder. The listing
property is false
by default.
The directory
handler has further options, like:
showHidden
: that determines if hidden files can be servedredirectToSlash
: determines that requests without trailing slash get redirected to the “with slash pendant”, which is useful for the resolution of relative pathslookupCompressed
: tells the file processor to search for the requested file in compressed.gz
versionetagMethod
: defines the method to calculate theETag
header. Please find more details about thedirectory
options within the inert GitHub repository.
Outlook
That’s the foundation you need to serve various types of files for incoming requests. You’ve learned how to prepare and add the capabilities to respond with file resources using the inert plugin. Furthermore, you know that there are multiple options to respond file requests using either the reply.file()
method or just define the path to a directory providing the actual files.
The next tutorial shifts direction to authentication and how to implement a cookie-based authentication and remember the users.
We appreciate feedback and love to help you if there’s a question on your mind. Let us know in the comments or on twitter @futurestud_io.
Make it rock & enjoy coding!
Additional Resources
- inert — static file and directory handler plugin for hapi