Bent is a functional HTTP client built around async/await that flips the traditional HTTP client model on its head. Instead of piling on features, Bent uses constraints—you define what's acceptable upfront, and anything outside those constraints throws an error.
At just 26.7 kB, it's a fraction of the size of Axios (174 kB) while delivering clean, predictable async code. In this guide, we'll walk through everything from basic GET requests to streaming file uploads.
Why Bent Is Different
Most HTTP clients like Axios or got focus on adding features. Bent takes the opposite approach—it's built around constraints.
You configure what responses are acceptable, and Bent automatically rejects anything else. This constraint-based model pairs perfectly with async/await's single return value pattern.
The library was created by Mikeal Rogers (the original author of the request
library) specifically to address limitations he encountered after 8 years of building HTTP clients. Bent works in both Node.js and browsers with zero polyfills.
Here's what makes it unique:
- Tiny footprint: 26.7 kB vs Axios at 174 kB
- Constraint-first API: Define acceptable responses, fail everything else
- Order-agnostic options: Pass options in any order—Bent figures it out
- Auto-typed responses: Built-in JSON, string, and buffer decoding
- Stream-friendly: First-class support for Node.js streams
Step 1: Install Bent
Open your terminal and install Bent via npm:
npm install bent
Or if you're using yarn:
yarn add bent
That's it. No additional dependencies needed. Bent is ready to use immediately in your Node.js project or can be bundled for browser use.
Step 2: Make Your First GET Request
Let's start with the simplest case—fetching JSON from an API. Bent returns a function that you await:
const bent = require('bent');
const getJSON = bent('json');
async function fetchUser() {
const user = await getJSON('https://api.github.com/users/torvalds');
console.log(user.name); // Linus Torvalds
}
fetchUser();
Here's what's happening behind the scenes. The bent('json')
call creates a specialized function that only accepts JSON responses with a 200 status code. If the server returns a 404 or sends XML, Bent throws an error automatically.
This constraint model eliminates entire classes of bugs. You never accidentally parse HTML as JSON because you forgot to check the Content-Type header.
Let's expand this with more constraints:
const getUser = bent('GET', 'https://api.github.com', 'json', 200);
async function fetchUser() {
const user = await getUser('/users/torvalds');
console.log(user.login);
}
Now we've locked down:
- HTTP method: Only GET
- Base URL:
https://api.github.com
- Response format: JSON only
- Status code: Must be 200
Notice the URL in the request is just /users/torvalds
. The base URL is prepended automatically.
Step 3: Handle Different Response Types
Bent supports three response formats out of the box: 'json'
, 'string'
, and 'buffer'
. Here's how to use each:
const bent = require('bent');
const getJSON = bent('json');
const getString = bent('string');
const getBuffer = bent('buffer');
async function fetchDifferentTypes() {
// Fetch and auto-parse JSON
const jsonData = await getJSON('https://api.example.com/data');
// Fetch HTML as string
const html = await getString('https://example.com');
// Fetch binary data (images, PDFs, etc)
const imageBuffer = await getBuffer('https://example.com/logo.png');
}
Each function is purpose-built. The JSON fetcher will fail if the response isn't valid JSON. The buffer fetcher returns raw bytes, perfect for binary data.
What if you don't specify a format? Bent returns the raw response stream:
const getStream = bent('https://api.example.com');
async function fetchStream() {
const stream = await getStream('/data');
console.log(stream.status); // 200
console.log(stream.statusCode); // 200 (alias)
// Decode manually if needed
const data = await stream.json();
// or
const text = await stream.text();
}
This is useful when you need response headers or want to pipe the stream directly to a file.
Step 4: POST, PUT, and DELETE Requests
Bent handles all HTTP methods. Specify the method as an uppercase string:
const bent = require('bent');
const post = bent('POST', 'https://api.example.com', 'json', 200);
const put = bent('PUT', 'json', 201);
const del = bent('DELETE', 204);
async function modifyData() {
// POST with JSON body
const newUser = await post('/users', {
name: 'Alice',
email: 'alice@example.com'
});
console.log(newUser);
// PUT to update
await put('https://api.example.com/users/123', {
name: 'Alice Updated'
});
// DELETE (expects 204 No Content)
await del('https://api.example.com/users/123');
}
When you pass a plain JavaScript object as the request body, Bent automatically:
- Stringifies it to JSON
- Sets
Content-Type: application/json
No manual JSON.stringify()
needed. This is one of those small conveniences that add up.
Form-Encoded POST Requests
If you need to POST form data instead of JSON, you'll need to handle encoding yourself:
const bent = require('bent');
const { encode } = require('form-urlencoded');
const postForm = bent('POST', 'string', 200, {
'Content-Type': 'application/x-www-form-urlencoded'
});
async function submitForm() {
const formData = encode({
username: 'alice',
password: 'secret'
});
const response = await postForm('https://example.com/login', formData);
console.log(response);
}
We set the Content-Type header manually and encode the body as a string. Bent sends it as-is.
Step 5: Configure Status Code Validation
By default, Bent only accepts status code 200. This is intentional—fail fast on unexpected responses. To accept additional codes, pass them as numbers:
const bent = require('bent');
// Accept both 200 and 201
const post = bent('POST', 'json', 200, 201);
// Accept any 2xx code
const get = bent('json', 200, 201, 202, 203, 204);
// Accept 200 and 404
const getOptional = bent('json', 200, 404);
async function fetchWithStatus() {
try {
const data = await post('https://api.example.com/items', { name: 'Widget' });
console.log('Created:', data);
} catch (err) {
console.error('Request failed:', err.message);
}
}
Important gotcha: Once you specify status codes, you must include 200 explicitly if you want it. This caught me off guard the first time:
// This ONLY accepts 201, not 200!
const post = bent('POST', 'json', 201);
// To accept both:
const post = bent('POST', 'json', 200, 201);
Status codes outside your specified range trigger an immediate error. This constraint-first approach forces you to think about edge cases upfront.
Step 6: Add Custom Headers
Headers can be set at two levels: globally when creating the function, or per-request.
Global headers:
const bent = require('bent');
const apiCall = bent('https://api.example.com', 'json', {
'Authorization': 'Bearer your-token-here',
'User-Agent': 'MyApp/1.0'
});
async function fetch() {
// These headers are sent automatically
const data = await apiCall('/protected-resource');
}
Per-request headers:
const apiCall = bent('POST', 'json');
async function upload() {
const data = await apiCall(
'https://api.example.com/upload',
{ file: 'data' },
{ 'X-Custom-Header': 'value' } // Third argument
);
}
The third argument to the returned function is always the headers object. This lets you override or add headers on a per-call basis.
Step 7: Handle Errors Properly
Bent throws errors for any response outside your constraints. This includes wrong status codes, unparseable JSON, and network failures.
Here's a robust error handling pattern:
const bent = require('bent');
const get = bent('GET', 'json', 200, 404);
async function fetchWithErrorHandling(url) {
try {
const data = await get(url);
return { success: true, data };
} catch (err) {
// Check if it's an HTTP error with a response
if (err.statusCode) {
console.error(`HTTP ${err.statusCode}: ${err.message}`);
// You can access the response body on errors
if (err.responseBody) {
const errorBody = await err.responseBody.text();
console.error('Server response:', errorBody);
}
return { success: false, statusCode: err.statusCode };
}
// Network error or other issue
console.error('Request failed:', err.message);
return { success: false, error: err.message };
}
}
fetchWithErrorHandling('https://api.example.com/data');
Bent attaches the statusCode
and responseBody
properties to errors when the server responds with an error status. This is incredibly useful for debugging API issues.
Common error scenarios:
// Unexpected status code
const get200Only = bent('json', 200);
await get200Only('https://api.example.com/not-found'); // Throws with statusCode 404
// Invalid JSON
const getJSON = bent('json');
await getJSON('https://example.com'); // Throws if response is HTML
// Network timeout
await getJSON('https://slow-api.example.com'); // Throws on timeout
Step 8: Upload and Download Streams
Bent has first-class support for Node.js streams, which is perfect for handling large files without loading them entirely into memory.
Uploading a file stream:
const bent = require('bent');
const fs = require('fs');
const upload = bent('PUT', 201);
async function uploadFile() {
const stream = fs.createReadStream('./large-file.zip');
await upload('https://api.example.com/upload', stream);
console.log('Upload complete');
}
Bent automatically pipes the stream as the request body. No need for multipart form handling in simple cases.
Downloading a file stream:
const bent = require('bent');
const fs = require('fs');
const download = bent(); // Returns stream by default
async function downloadFile() {
const response = await download('https://example.com/large-file.zip');
// Pipe directly to file
const writeStream = fs.createWriteStream('./downloaded-file.zip');
response.pipe(writeStream);
writeStream.on('finish', () => {
console.log('Download complete');
});
}
When you don't specify a response format, Bent returns the raw stream. This is efficient for large downloads—you process data as it arrives rather than buffering it all in memory.
A more robust download with progress tracking:
const bent = require('bent');
const fs = require('fs');
async function downloadWithProgress(url, outputPath) {
const download = bent();
const response = await download(url);
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
const writeStream = fs.createWriteStream(outputPath);
response.on('data', (chunk) => {
downloaded += chunk.length;
const progress = ((downloaded / totalSize) * 100).toFixed(2);
console.log(`Progress: ${progress}%`);
});
response.pipe(writeStream);
return new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
}
downloadWithProgress(
'https://example.com/big-file.zip',
'./output.zip'
);
This pattern works for any large file download where you need progress tracking or want to avoid memory issues.
Step 9: Chain Multiple Constraints
One of Bent's most powerful features is how it lets you compose constraints. Here's a real-world example for a REST API client:
const bent = require('bent');
// Base client for all requests
const api = bent('https://api.example.com/v1', {
'Authorization': 'Bearer token',
'Accept': 'application/json'
});
// Specialized clients for different operations
const getJSON = bent('GET', 'https://api.example.com/v1', 'json', 200, {
'Authorization': 'Bearer token'
});
const postJSON = bent('POST', 'https://api.example.com/v1', 'json', 200, 201, {
'Authorization': 'Bearer token'
});
const deleteResource = bent('DELETE', 'https://api.example.com/v1', 204, {
'Authorization': 'Bearer token'
});
// Usage
async function manageResources() {
const items = await getJSON('/items');
console.log('Items:', items);
const newItem = await postJSON('/items', { name: 'Widget' });
console.log('Created:', newItem);
await deleteResource(`/items/${newItem.id}`);
console.log('Deleted');
}
Each function has exactly the constraints it needs. The GET client only accepts 200, the POST client accepts 200 or 201, and the DELETE client expects 204 No Content.
This approach scales well. You can build a library of pre-configured functions for each API endpoint, each with its own rules.
Step 10: Work Around the 204 No Content Edge Case
There's a quirk with Bent and 204 responses that you should know about. A 204 status means "success, but no content," which creates an ambiguity: What should Bent return?
If you try this:
const del = bent('DELETE', 204);
await del('https://api.example.com/item/123');
Bent might throw "Unknown body type" because it doesn't know how to decode an empty response. The fix is to either accept the raw response or specify a format:
// Option 1: Return raw response
const del = bent('DELETE', 204);
const response = await del('https://api.example.com/item/123');
console.log(response.statusCode); // 204
// Option 2: Accept as string (which will be empty)
const del = bent('DELETE', 'string', 204);
await del('https://api.example.com/item/123'); // Returns ""
This is a known issue that's been discussed in the Bent GitHub repo. The workaround is simple once you know about it.
Common Pitfalls and How to Avoid Them
After using Bent in production, here are the gotchas I've encountered:
Pitfall 1: Forgetting to include 200 when specifying status codes
// Wrong - only accepts 201
const post = bent('POST', 'json', 201);
// Right - accepts both
const post = bent('POST', 'json', 200, 201);
Pitfall 2: Not handling errors with responseBody
When Bent throws on a bad status code, the error object includes the response body. You need to decode it:
try {
await get('https://api.example.com/endpoint');
} catch (err) {
if (err.responseBody) {
const body = await err.responseBody.text(); // Don't forget to await
console.error('Error body:', body);
}
}
Pitfall 3: Mixing up response types
If you specify 'json'
but the server sends plain text, Bent throws. Always match the format to what the server actually returns, not what you hope it returns.
Pitfall 4: Base URLs and trailing slashes
// These work differently
const api1 = bent('https://api.example.com');
await api1('/users'); // https://api.example.com/users ✓
const api2 = bent('https://api.example.com/');
await api2('users'); // https://api.example.com/users ✓
await api2('/users'); // https://api.example.com//users ✗
Watch your slashes. Bent doesn't normalize them, so you'll get double slashes if you're not careful.
When to Use Bent vs Alternatives
Use Bent when:
- You want minimal bundle size (great for serverless functions)
- Your API has well-defined status code contracts
- You're building async/await-heavy code
- You need stream support with clean syntax
- You want to fail fast on unexpected responses
Use Axios when:
- You need interceptors for request/response transformation
- You want automatic request cancellation
- You need progress events for uploads
- Your team is already familiar with Axios
- You need IE11 support
Use native fetch when:
- You're only targeting modern browsers/Node.js 18+
- You want zero dependencies
- You don't mind writing more boilerplate
Wrapping Up
Bent's constraint-based model is a paradigm shift from traditional HTTP clients. Instead of bolting on features, you define acceptable responses upfront and let Bent handle the rest. This approach results in cleaner, more predictable async code with fewer edge cases.
The library's 26.7 kB footprint makes it ideal for serverless environments where cold start times matter. Combined with first-class stream support and automatic response typing, Bent offers a compelling alternative to heavier clients like Axios.
Start with simple GET requests, add constraints as needed, and lean on Bent's error-by-default philosophy to catch bugs early. Your future self will thank you when you're not debugging why your API client silently accepted an HTML error page as valid JSON.