@just-io/server
v1.0.2
Published
Simple web server
Readme
@just-io/server
Introduction
A lightweight TypeScript web server framework with routing, middleware support, body parsing, JWT utilities, and file handling.
Installation
npm install @just-io/serverThis is a TypeScript module.
Features
- Routing with pattern matching and URL parameters
- Middleware support for authentication and request processing
- Body parsing for JSON, FormData (with file uploads), text, and URL-encoded data
- Response types: text, JSON, file (with attachment support)
- Rate limiting and request timeout support
- JWT encoding, decoding, and verification
- File storage utilities
Usage
Basic Server Setup
import http from 'node:http';
import { Server, MiddlewarelessRouter, makeLocalFileStorage } from '@just-io/server';
const { createFileLocation } = makeLocalFileStorage('./temp');
const server = new Server(new http.Server(), {
createFileLocation,
makeGlobal: () => Promise.resolve(),
});
server.addRouter(
'/api',
new MiddlewarelessRouter()
.get('/hello', () => Promise.resolve({
body: { type: 'text', content: 'Hello World' }
}))
);
server.listen(8080);Response Types
Empty Response
.get('/empty', () => Promise.resolve({}))Text Response
.get('/text', () => Promise.resolve({
body: { type: 'text', content: 'Hello World' }
}))JSON Response
.get('/json', () => Promise.resolve({
body: { type: 'json', content: { key: 'value' } }
}))File Response
.get('/file', () => Promise.resolve({
body: {
type: 'file',
contentType: 'text/plain',
content: { type: 'text', content: 'file contents' }
}
}))File Download (Attachment)
.get('/download', () => Promise.resolve({
body: {
type: 'file',
contentType: 'text/plain',
content: { type: 'text', content: 'file contents' },
attachment: { filename: 'document.txt' }
}
}))URL Parameters
Search Parameters
.get('/search', (netRequest) => {
const params = Array.from(netRequest.url.searchParams.entries());
return Promise.resolve({
body: { type: 'json', content: params }
});
})
// GET /search?param_1=1¶m_2=2 -> [["param_1","1"],["param_2","2"]]Path Parameters
.get('/users/:id', (netRequest) => {
const userId = netRequest.pathname.groups.id;
return Promise.resolve({
body: { type: 'json', content: { id: userId } }
});
})
// GET /users/123 -> { id: "123" }Request Body Handling
JSON Body
.post('/json', (netRequest) => {
if (netRequest.body?.type !== 'json') {
return Promise.reject(new NetResponseError(400, {
type: 'text',
content: 'Body should be JSON'
}));
}
return Promise.resolve({
body: { type: 'json', content: netRequest.body.content }
});
})FormData Body
.post('/form', (netRequest) => {
if (netRequest.body?.type !== 'form-data') {
return Promise.reject(new NetResponseError(400));
}
return Promise.resolve({
body: { type: 'json', content: netRequest.body.formValues }
});
})
// FormData { first: "value", second: ["a", "b"] }
// -> { first: ["value"], second: ["a", "b"] }FormData with Files
.post('/upload', (netRequest) => {
if (netRequest.body?.type !== 'form-data') {
return Promise.reject(new NetResponseError(400));
}
// Files have: filename, size, type, location
const files = Object.values(netRequest.body.formValues)
.flat()
.filter(v => typeof v !== 'string')
.map(f => [f.filename, f.size, f.type]);
return Promise.resolve({
body: { type: 'json', content: files }
});
})Route Options
Accept Content Types
.post('/strict-json', {
options: { acceptContentTypes: ['application/json'] },
handle: (netRequest) => Promise.resolve({
body: { type: 'json', content: netRequest.body.content }
})
})
// Returns 406 Not Acceptable if content-type doesn't matchMax Content Length
.post('/limited', {
options: { maxContentLength: 1024 },
handle: () => Promise.resolve({})
})
// Returns 413 Content Too Large if body exceeds limitRate Limiting
.get('/rate-limited', {
options: {
shouldAbort: (req) => {
// Return true to abort with 429 Too Many Requests
return Promise.resolve(isRateLimited(req));
}
},
handle: () => Promise.resolve({})
})Request Timeout
.get('/with-timeout', {
options: { timeout: 5000 }, // 5 seconds
handle: () => Promise.resolve({})
})
// Returns 504 Gateway Timeout if handler takes too longMiddleware and Context
Router with Middleware
import { Router, NetRequest, NetResponseError, updateContext } from '@just-io/server';
type UserContext = { id: string; token: string };
const authRouter = new Router<unknown, UserContext>(
(netRequest: NetRequest<unknown>): Promise<NetRequest<unknown, UserContext>> => {
const authorization = netRequest.headers.authorization;
if (!authorization) {
return Promise.reject(new NetResponseError(403));
}
const token = authorization.slice('Bearer '.length);
try {
const userInfo = JSON.parse(atob(token));
return Promise.resolve(updateContext(netRequest, {
id: userInfo.id,
token,
}));
} catch {
return Promise.reject(new NetResponseError(400, {
type: 'text',
content: 'Invalid authorization token'
}));
}
}
);
authRouter.get('/user-info', (netRequest) => {
return Promise.resolve({
body: {
type: 'json',
content: { userId: netRequest.context.id }
}
});
});
server.addRouter('/auth', authRouter);Global State
type GlobalInfo = { serverName: string };
const server = new Server<GlobalInfo>(new http.Server(), {
createFileLocation,
makeGlobal: () => Promise.resolve({ serverName: 'my-server' }),
});
server.addRouter(
'/info',
new MiddlewarelessRouter<GlobalInfo>().get('', (netRequest) => {
return Promise.resolve({
body: {
type: 'json',
content: { server: netRequest.global.serverName }
}
});
})
);Request Finished Callback
import { RequestProcessingInfo } from '@just-io/server';
const server = new Server(new http.Server(), {
createFileLocation,
makeGlobal: () => Promise.resolve(),
onRequestFinished: (request, info: RequestProcessingInfo) => {
console.log(`Request finished: ${info.finishedReason}`);
return Promise.resolve();
}
});JWT Utilities
import { encode, decode, verify, check } from '@just-io/server';
const privateKey = 'your-secret-key';
// Encode
const token = encode({ userId: '24' }, privateKey);
// -> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIyNCJ9...."
// Decode (without verification)
const body = decode(token); // -> '{"userId":"24"}'
const parsed = decode(token, true); // -> { userId: '24' }
// Verify
const isValid = verify(token, privateKey); // -> true or false
// Check (verify + decode)
const result = check(token, privateKey, true);
if (result.ok) {
console.log(result.value); // -> { userId: '24' }
} else {
console.log(result.error); // -> 'invalid-sign'
}Pattern Matching
import Pattern from '@just-io/server';
// Exact match
const exact = new Pattern('/users');
exact.exec('/users'); // -> { matched: '/users', groups: {} }
exact.exec('/notes'); // -> null
// Wildcard
const wildcard = new Pattern('/users/*');
wildcard.exec('/users/12/notes/24'); // -> { matched: '/users/12/notes/24', groups: {} }
// Named parameters
const params = new Pattern('/users/:user-id/notes/:note-id');
params.exec('/users/12/notes/24');
// -> { matched: '/users/12/notes/24', groups: { 'user-id': '12', 'note-id': '24' } }MIME Type Utilities
import { getMimeTypeByFileExtension, UNKNOWN_MIME_TYPE } from '@just-io/server';
getMimeTypeByFileExtension('file.txt'); // -> 'text/plain'
getMimeTypeByFileExtension('file.js'); // -> 'text/javascript'
getMimeTypeByFileExtension('file.tar'); // -> 'application/x-tar'
getMimeTypeByFileExtension('file.xyz'); // -> UNKNOWN_MIME_TYPEError Handling
Use NetResponseError to return HTTP errors with optional body:
import { NetResponseError } from '@just-io/server';
// Simple error
throw new NetResponseError(404);
// Error with message
throw new NetResponseError(400, {
type: 'text',
content: 'Invalid request'
});
// Error with JSON body
throw new NetResponseError(422, {
type: 'json',
content: { errors: ['field is required'] }
});HTTP Status Codes
The server handles these status codes automatically:
200- Success400- Bad Request (invalid JSON, etc.)403- Forbidden404- Not Found406- Not Acceptable (content type mismatch)413- Content Too Large429- Too Many Requests (rate limited)500- Internal Server Error504- Gateway Timeout
Testing
npm testLicense
See LICENSE file.
