Lieko Express Documentation
A modern, minimal, Express-like framework for Node.js with built-in body parsing, CORS, validation, and more
Introduction
Lieko-express is a lightweight, fast, and feature-rich web framework for Node.js. It provides a familiar Express-like API while offering modern features out of the box.
Key Features
-
Express-compatible API
Familiar routing, middleware, and request/response handling
-
Built-in Body Parsing
JSON, URL-encoded, and multipart form-data support
-
CORS Support
Configurable cross-origin resource sharing with flexible options
-
Schema Validation
Built-in validation system with comprehensive validators
-
Static File Serving
Efficient static file middleware with caching and ETags
-
Template Engine
Simple HTML templating with support for custom engines
-
Route Groups
Organize routes with nested groups and shared middleware
-
Debug Mode
Detailed request logging with timing and payload information
Installation
NPM
npm install lieko-express
Yarn
yarn add lieko-express
Requirements
-
Node.js
≥14.0.0
Minimum Node.js version required
Quick Start
Create your first Lieko-express application in seconds.
Hello World
const Lieko = require('lieko-express');
const app = Lieko();
app.get('/', (req, res) => {
res.json({ message: 'Hello World!' });
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Basic REST API
const Lieko = require('lieko-express');
const app = Lieko();
// Enable debug mode
app.debug(true);
// Simple in-memory database
const users = [];
let idCounter = 1;
// Get all users
app.get('/api/users', (req, res) => {
res.json(users);
});
// Get user by ID
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json(user);
});
// Create user
app.post('/api/users', (req, res) => {
const user = {
id: String(idCounter++),
name: req.body.name,
email: req.body.email,
createdAt: new Date().toISOString()
};
users.push(user);
res.status(201).json(user);
});
// Update user
app.patch('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
Object.assign(user, req.body);
res.json(user);
});
// Delete user
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === req.params.id);
if (index === -1) {
return res.status(404).json({
error: 'User not found'
});
}
users.splice(index, 1);
res.status(204).end();
});
app.listen(3000, () => {
console.log('API running on http://localhost:3000');
});
Configuration
Customize Lieko-express behavior with various settings.
Application Settings
const app = Lieko();
// Enable/disable features
app.set('x-powered-by', 'MyApp');
app.set('trust proxy', true);
app.set('views', './templates');
app.set('view engine', 'html');
// Enable debug mode
app.debug(true);
// Disable strict trailing slash
app.set('strictTrailingSlash', false);
app.set('allowTrailingSlash', true);
Configuration Options
| Setting | Type | Default | Description |
|---|---|---|---|
debug |
boolean | false | Enable detailed request logging |
x-powered-by |
string|boolean | 'lieko-express' | X-Powered-By header value |
trust proxy |
boolean | false | Trust X-Forwarded-* headers |
strictTrailingSlash |
boolean | true | Strict trailing slash matching |
allowTrailingSlash |
boolean | true | Allow optional trailing slash |
views |
string | './views' | Template directory path |
view engine |
string | 'html' | Default template engine |
Getting/Setting Values
// Set a value
app.set('myOption', 'value');
// Get a value
const value = app.get('myOption');
// Enable/disable settings
app.enable('trust proxy');
app.disable('x-powered-by');
// Check if enabled
if (app.enabled('trust proxy')) {
// ...
}
// Check if disabled
if (app.disabled('debug')) {
// ...
}
Routing
Define routes to handle HTTP requests with flexible patterns and parameters.
Basic Routes
// GET request
app.get('/users', (req, res) => {
res.json({ users: [] });
});
// POST request
app.post('/users', (req, res) => {
res.status(201).json({ message: 'User created' });
});
// PUT request
app.put('/users/:id', (req, res) => {
res.json({ message: 'User updated' });
});
// PATCH request
app.patch('/users/:id', (req, res) => {
res.json({ message: 'User patched' });
});
// DELETE request
app.delete('/users/:id', (req, res) => {
res.status(204).end();
});
// Handle all methods
app.all('/admin', (req, res) => {
res.json({ method: req.method });
});
Route Parameters
// Single parameter
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.json({ userId });
});
// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (req, res) => {
const { postId, commentId } = req.params;
res.json({ postId, commentId });
});
// Optional segments with wildcards
app.get('/files/*', (req, res) => {
// Matches /files/documents/report.pdf
res.send('File route');
});
Multiple Handlers
// Middleware before handler
app.get('/protected',
authMiddleware,
(req, res) => {
res.json({ message: 'Protected route' });
}
);
// Multiple middleware
app.post('/api/data',
validateToken,
checkPermissions,
parseData,
(req, res) => {
res.json({ success: true });
}
);
Multiple Paths
// Same handler for multiple routes
app.get(['/home', '/', '/index'], (req, res) => {
res.send('Homepage');
});
Middleware
Middleware functions execute in order and can modify requests and responses.
Application-level Middleware
// Executed for all routes
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Mounted on specific path
app.use('/api', (req, res, next) => {
console.log('API route accessed');
next();
});
Route-level Middleware
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({
error: 'No token provided'
});
}
// Verify token...
req.user = { id: '123', name: 'Alice' };
next();
};
// Apply to specific routes
app.get('/profile', authenticate, (req, res) => {
res.json(req.user);
});
Error Handling Middleware
// Error handler (4 parameters)
app.errorHandler((err, req, res, next) => {
console.error('Error:', err.message);
res.status(err.status || 500).json({
error: {
message: err.message,
code: err.code || 'SERVER_ERROR'
}
});
});
Built-in Middleware
// Static files
app.use(app.static('public'));
// CORS
app.cors({
origin: '*',
methods: ['GET', 'POST'],
credentials: true
});
next() in
middleware unless you're sending a response. Missing next()
will cause requests to hang.
Request Object
The request object contains information about the HTTP request.
Properties
| Property | Type | Description |
|---|---|---|
req.body |
object | Parsed request body (JSON, form data) |
req.params |
object | Route parameters |
req.query |
object | Query string parameters |
req.files |
object | Uploaded files (multipart) |
req.headers |
object | HTTP headers |
req.method |
string | HTTP method (GET, POST, etc.) |
req.url |
string | Request URL |
req.originalUrl |
string | Original request URL |
req.path |
string | Request path (without query string) |
req.ip |
object | Client IP address info |
req.protocol |
string | Request protocol (http/https) |
req.secure |
boolean | True if HTTPS |
req.hostname |
string | Host name |
req.subdomains |
array | Subdomain array |
req.xhr |
boolean | True if XMLHttpRequest |
req.bearer |
string|null | Bearer token from Authorization header |
Methods
// Get header value
const contentType = req.get('Content-Type');
const auth = req.header('Authorization');
// Check accepted content types
if (req.accepts(['json', 'html'])) {
// Client accepts JSON or HTML
}
// Check accepted languages
const lang = req.acceptsLanguages(['en', 'fr']);
// Check accepted encodings
const encoding = req.acceptsEncodings(['gzip', 'deflate']);
// Check accepted charsets
const charset = req.acceptsCharsets(['utf-8']);
// Check content type
if (req.is('json')) {
// Request body is JSON
}
if (req.is('application/json')) {
// Exact content type match
}
IP Address
// IP address information
console.log(req.ip);
// {
// raw: '::ffff:127.0.0.1',
// ipv4: '127.0.0.1',
// ipv6: null,
// display: '127.0.0.1'
// }
// IP chain (with proxies)
console.log(req.ips);
// ['203.0.113.1', '198.51.100.1']
Query Parameters
// URL: /search?q=hello&page=2&active=true
app.get('/search', (req, res) => {
console.log(req.query);
// {
// q: 'hello',
// page: 2, // Auto-converted to number
// active: true // Auto-converted to boolean
// }
res.json(req.query);
});
Response Object
The response object provides methods to send responses to clients.
Sending Responses
// Send JSON
app.get('/json', (req, res) => {
res.json({ message: 'Hello' });
});
// Send plain text
app.get('/text', (req, res) => {
res.send('Hello World');
});
// Send HTML
app.get('/html', (req, res) => {
res.html('Hello
');
});
// Send file
app.get('/download', (req, res) => {
res.sendFile('./files/document.pdf');
});
// End response
app.get('/empty', (req, res) => {
res.end();
});
Status Codes
// Set status and send
app.post('/users', (req, res) => {
res.status(201).json({
message: 'Created'
});
});
// Chainable methods
app.get('/error', (req, res) => {
res.status(500)
.type('application/json')
.send({ error: 'Server error' });
});
Response Helpers
// Success response (200)
res.ok({ data: users });
res.ok({ data: users }, 'Users retrieved');
// Created (201)
res.created({ user }, 'User created');
// No content (204)
res.noContent();
// Accepted (202)
res.accepted({ task }, 'Task queued');
// Paginated response
res.paginated(
items, // Array of items
total, // Total count
'Data retrieved' // Optional message
);
// Error responses
res.badRequest('Invalid input');
res.unauthorized('Authentication required');
res.forbidden('Access denied');
res.notFound('Resource not found');
res.serverError('Internal error');
// Custom error
res.error({
message: 'Validation failed',
code: 'VALIDATION_ERROR',
status: 400,
details: errors
});
Headers
// Set single header
res.setHeader('X-Custom', 'value');
res.header('X-Another', 'value');
// Set multiple headers
res.set({
'X-Custom': 'value',
'X-Another': 'value'
});
// Set content type
res.type('application/json');
res.type('text/html');
// Remove header
res.removeHeader('X-Custom');
Redirects
// Temporary redirect (302)
res.redirect('/new-url');
// Permanent redirect (301)
res.redirect('/new-url', 301);
// Other status codes
res.redirect('/temp', 307); // Temporary, preserve method
res.redirect('/perm', 308); // Permanent, preserve method
File Download
// Send file with options
res.sendFile('./files/document.pdf', {
maxAge: 3600000, // Cache for 1 hour
lastModified: true, // Send Last-Modified header
acceptRanges: true, // Support range requests
dotfiles: 'ignore', // Ignore dotfiles
headers: {
'X-Custom': 'value'
}
}, (err) => {
if (err) {
console.error('File send error:', err);
}
});
Template Rendering
// Render template
res.render('index', {
title: 'My Page',
user: { name: 'Alice' }
});
// With callback
res.render('index', { title: 'Page' }, (err, html) => {
if (err) {
return res.status(500).send('Render error');
}
// Modify html if needed
res.html(html);
});
Body Parsing
Lieko-express automatically parses request bodies for JSON, URL-encoded, and multipart data.
Configuration
// Configure all parsers
app.bodyParser({
limit: '50mb', // Size limit for all types
extended: true, // URL-encoded extended mode
strict: true // JSON strict mode
});
// Configure individually
app.json({
limit: '10mb',
strict: false
});
app.urlencoded({
limit: '5mb',
extended: true
});
app.multipart({
limit: '100mb'
});
JSON Body
// Automatic JSON parsing
app.post('/api/users', (req, res) => {
console.log(req.body);
// { name: 'Alice', email: 'alice@example.com' }
res.json({
received: req.body
});
});
URL-encoded Body
// Extended mode: supports nested objects
app.post('/form', (req, res) => {
console.log(req.body);
// {
// name: 'Alice',
// address: {
// city: 'Paris',
// country: 'FR'
// }
// }
res.json(req.body);
});
Multipart Form Data
// Files and fields
app.post('/upload', (req, res) => {
// Access uploaded files
console.log(req.files);
// {
// avatar: {
// filename: 'photo.jpg',
// data: Buffer,
// size: 45120,
// contentType: 'image/jpeg'
// }
// }
// Access form fields
console.log(req.body);
// { username: 'alice', bio: 'Developer' }
res.json({
uploaded: Object.keys(req.files),
fields: req.body
});
});
Body Size Limits
| Type | Default Limit | Format |
|---|---|---|
| JSON | 10mb | Number or string (e.g., '10mb', '5kb') |
| URL-encoded | 10mb | Number or string |
| Multipart | 10mb | Number or string |
CORS
Configure Cross-Origin Resource Sharing with flexible options.
Basic Setup
// Enable CORS for all routes
app.cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
headers: ['Content-Type', 'Authorization'],
credentials: false
});
Advanced Configuration
app.cors({
origin: ['https://example.com', 'https://app.example.com'],
strictOrigin: true, // Reject unknown origins
methods: ['GET', 'POST'],
headers: ['Content-Type', 'X-Custom-Header'],
credentials: true,
maxAge: 86400, // Preflight cache (24h)
exposedHeaders: ['X-Total-Count'],
allowPrivateNetwork: true, // Chrome private network access
debug: true // Log CORS decisions
});
Wildcard Origins
// Allow subdomains
app.cors({
origin: '*.example.com'
});
// Multiple patterns
app.cors({
origin: ['*.example.com', '*.test.com']
});
Dynamic Origins
// Function-based origin validation
app.cors({
origin: (origin) => {
const whitelist = ['https://example.com'];
return whitelist.includes(origin);
}
});
Route-specific CORS
// Disable CORS for specific route
app.get('/internal', (req, res) => {
// This route won't have CORS headers
res.json({ message: 'Internal only' });
});
// Custom CORS per route (coming soon)
// app.get('/api/data',
// { cors: { origin: 'https://app.example.com' } },
// (req, res) => {
// res.json({ data: [] });
// }
// );
CORS Options
| Option | Type | Default | Description |
|---|---|---|---|
origin |
string|array|function | '*' | Allowed origins |
strictOrigin |
boolean | false | Reject requests from unknown origins |
methods |
array | ['GET', 'POST', ...] | Allowed HTTP methods |
headers |
array | ['Content-Type', ...] | Allowed request headers |
credentials |
boolean | false | Allow credentials (cookies, auth) |
maxAge |
number | 86400 | Preflight cache duration (seconds) |
exposedHeaders |
array | [] | Headers exposed to client |
allowPrivateNetwork |
boolean | false | Chrome private network access |
debug |
boolean | false | Log CORS decisions |
Static Files
Serve static files with caching, ETags, and range support.
Basic Usage
// Serve files from 'public' directory
app.use(app.static('public'));
// Mount on specific path
app.use('/static', app.static('public'));
// Multiple directories
app.use(app.static('public'));
app.use('/assets', app.static('assets'));
Configuration
app.use(app.static('public', {
maxAge: 86400000, // Cache for 24 hours
index: 'index.html', // Default index file
dotfiles: 'ignore', // 'allow', 'deny', 'ignore'
etag: true, // Enable ETag headers
lastModified: true, // Send Last-Modified header
extensions: ['html'], // Try these extensions
fallthrough: true, // Continue to next middleware if not found
immutable: true, // Add immutable cache directive
cacheControl: true, // Enable Cache-Control header
redirect: true, // Redirect to trailing slash for directories
setHeaders: (res, path, stat) => {
// Custom headers
res.setHeader('X-Custom', 'value');
}
}));
Index Files
// Single index file
app.use(app.static('public', {
index: 'index.html'
}));
// Multiple index files (try in order)
app.use(app.static('public', {
index: ['index.html', 'index.htm', 'default.html']
}));
// Disable index files
app.use(app.static('public', {
index: false
}));
Caching
// Long-term caching for static assets
app.use('/assets', app.static('public/assets', {
maxAge: 31536000000, // 1 year
immutable: true
}));
// Short-term caching for dynamic content
app.use('/content', app.static('public/content', {
maxAge: 3600000 // 1 hour
}));
Security
// Prevent access to dotfiles
app.use(app.static('public', {
dotfiles: 'deny' // Return 403 for dotfiles
}));
// Ignore dotfiles (404)
app.use(app.static('public', {
dotfiles: 'ignore'
}));
Static Options
| Option | Type | Default | Description |
|---|---|---|---|
maxAge |
number | 0 | Cache max-age in milliseconds |
index |
string|array|false | 'index.html' | Directory index file(s) |
dotfiles |
string | 'ignore' | How to handle dotfiles |
etag |
boolean | true | Enable ETag generation |
lastModified |
boolean | true | Send Last-Modified header |
extensions |
array|false | false | File extensions to try |
fallthrough |
boolean | true | Continue to next middleware if not found |
immutable |
boolean | false | Add immutable cache directive |
redirect |
boolean | true | Redirect to trailing slash for dirs |
setHeaders |
function | null | Function to set custom headers |
Template Engine
Built-in HTML templating with support for custom engines.
Setup
// Configure views directory
app.set('views', './templates');
app.set('view engine', 'html');
Built-in Templating
// Template file: views/index.html
// <!DOCTYPE html>
// <html>
// <head>
// <title>{{ title }}</title>
// </head>
// <body>
// <h1>{{ heading }}</h1>
// <p>Welcome, {{ username }}!</p>
// </body>
// </html>
// Render template
app.get('/', (req, res) => {
res.render('index', {
title: 'My Page',
heading: 'Welcome',
username: 'Alice'
});
});
Safe vs Unsafe Output
// Safe output (escaped HTML)
// {{ variable }}
// Unsafe output (raw HTML)
// {{{ variable }}}
app.get('/post', (req, res) => {
res.render('post', {
title: 'Blog Post',
content: '<p>HTML content</p>', // Will be escaped with {{ }}
rawHtml: '<p>HTML content</p>' // Will be raw with {{{ }}}
});
});
Custom Template Engine
// Register EJS engine
const ejs = require('ejs');
app.engine('ejs', (filePath, options, callback) => {
ejs.renderFile(filePath, options, callback);
});
app.set('view engine', 'ejs');
// Render EJS template
app.get('/page', (req, res) => {
res.render('page', {
data: []
});
});
Response Locals
// Set global variables for all templates
app.use((req, res, next) => {
res.locals.appName = 'MyApp';
res.locals.year = new Date().getFullYear();
res.locals.user = req.user || null;
next();
});
// Available in all templates
app.get('/page', (req, res) => {
res.render('page', {
// Additional page-specific data
pageTitle: 'Home'
});
});
Render with Callback
app.get('/page', (req, res) => {
res.render('page', { title: 'Page' }, (err, html) => {
if (err) {
console.error('Render error:', err);
return res.status(500).send('Template error');
}
// Modify HTML before sending
const modified = html.replace('{{year}}', '2026');
res.html(modified);
});
});
Schema Validation
Built-in request validation with comprehensive validators.
Basic Validation
const { Schema, validators: v, validate } = require('lieko-express');
// Create schema
const userSchema = new Schema({
username: [v.required(), v.string(), v.minLength(3)],
email: [v.required(), v.email()],
age: [v.number(), v.positive(), v.min(18)]
});
// Apply validation middleware
app.post('/users', validate(userSchema), (req, res) => {
// req.body is validated
res.json({
message: 'User created',
data: req.body
});
});
Validation Response
// Invalid request
// POST /users
// { "username": "ab", "email": "invalid" }
// Response (400)
{
"success": false,
"message": "Validation failed",
"errors": [
{
"field": "username",
"message": "Field must be at least 3 characters",
"type": "minLength"
},
{
"field": "email",
"message": "Invalid email format",
"type": "email"
}
]
}
Partial Validation
const { validatePartial } = require('lieko-express');
// Create base schema
const userSchema = new Schema({
username: [v.required(), v.string(), v.minLength(3)],
email: [v.required(), v.email()],
password: [v.required(), v.minLength(8)]
});
// For PATCH requests (optional fields)
const userUpdateSchema = validatePartial(userSchema);
app.patch('/users/:id',
validate(userUpdateSchema),
(req, res) => {
// Only provided fields are validated
res.json({ message: 'User updated' });
}
);
Available Validators
| Validator | Description | Example |
|---|---|---|
required() |
Field must be present | v.required('Name is required') |
optional() |
Field is optional | v.optional() |
string() |
Must be string | v.string() |
number() |
Must be number | v.number() |
boolean() |
Must be boolean | v.boolean() |
integer() |
Must be integer | v.integer() |
email() |
Valid email format | v.email() |
min(n) |
Minimum value/length | v.min(18) |
max(n) |
Maximum value/length | v.max(100) |
minLength(n) |
Minimum string length | v.minLength(3) |
maxLength(n) |
Maximum string length | v.maxLength(50) |
length(n) |
Exact string length | v.length(10) |
positive() |
Must be positive | v.positive() |
negative() |
Must be negative | v.negative() |
pattern(regex) |
Match regex pattern | v.pattern(/^[A-Z]/) |
oneOf(values) |
Must be one of values | v.oneOf(['admin', 'user']) |
notOneOf(values) |
Must not be one of values | v.notOneOf(['banned']) |
equal(value) |
Must equal value | v.equal('expected') |
mustBeTrue() |
Must be true | v.mustBeTrue('Accept terms') |
mustBeFalse() |
Must be false | v.mustBeFalse() |
date() |
Valid date | v.date() |
before(date) |
Date before specified | v.before('2026-12-31') |
after(date) |
Date after specified | v.after('2026-01-01') |
startsWith(str) |
String starts with value | v.startsWith('user_') |
endsWith(str) |
String ends with value | v.endsWith('.com') |
custom(fn) |
Custom validation function | v.custom((val) => val !== 'admin') |
Complex Validation
const { Schema, validators: v } = require('lieko-express');
// Nested validation
const addressSchema = new Schema({
street: [v.required(), v.string()],
city: [v.required(), v.string()],
zipCode: [v.required(), v.pattern(/^\d{5}$/)]
});
const userSchema = new Schema({
username: [
v.required(),
v.string(),
v.minLength(3),
v.maxLength(20),
v.pattern(/^[a-zA-Z0-9_]+$/, 'Only alphanumeric and underscore')
],
email: [v.required(), v.email()],
age: [v.number(), v.positive(), v.min(18), v.max(120)],
role: [v.required(), v.oneOf(['admin', 'user', 'guest'])],
termsAccepted: [v.required(), v.mustBeTrue()],
password: [
v.required(),
v.minLength(8),
v.pattern(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain uppercase, lowercase and number'
)
],
confirmPassword: [
v.required(),
v.custom((value, data) => value === data.password, 'Passwords must match')
]
});
app.post('/register', validate(userSchema), (req, res) => {
res.created(req.body, 'User registered successfully');
});
Custom Error Messages
const schema = new Schema({
username: [
v.required('Username is required'),
v.minLength(3, 'Username must be at least 3 characters long'),
v.pattern(/^[a-z]+$/, 'Username must be lowercase letters only')
],
age: [
v.required('Age is required'),
v.number('Age must be a valid number'),
v.min(18, 'You must be at least 18 years old')
]
});
Route Groups
Organize routes with shared prefixes and middleware.
Basic Groups
const app = Lieko();
// Group API routes
app.group('/api', (api) => {
api.get('/users', (req, res) => {
res.json({ users: [] });
});
api.post('/users', (req, res) => {
res.created({ user: {} });
});
api.get('/posts', (req, res) => {
res.json({ posts: [] });
});
});
Groups with Middleware
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.unauthorized('No token provided');
}
// Verify token...
req.user = { id: '123', role: 'user' };
next();
};
// Admin middleware
const requireAdmin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.forbidden('Admin access required');
}
next();
};
// Protected admin routes
app.group('/admin', authenticate, requireAdmin, (admin) => {
admin.get('/dashboard', (req, res) => {
res.json({ message: 'Admin dashboard' });
});
admin.get('/users', (req, res) => {
res.json({ users: [] });
});
admin.delete('/users/:id', (req, res) => {
res.json({ message: 'User deleted' });
});
});
Nested Groups
app.group('/api/v1', (v1) => {
// Public routes
v1.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Protected routes
v1.group('/protected', authenticate, (protected) => {
protected.get('/profile', (req, res) => {
res.json({ user: req.user });
});
// Admin routes nested within protected
protected.group('/admin', requireAdmin, (admin) => {
admin.get('/stats', (req, res) => {
res.json({ stats: {} });
});
admin.post('/settings', (req, res) => {
res.json({ message: 'Settings updated' });
});
});
});
});
Mounting Routers
// users.js
const Lieko = require('lieko-express');
const usersRouter = Lieko.Router();
usersRouter.get('/', (req, res) => {
res.json({ users: [] });
});
usersRouter.get('/:id', (req, res) => {
res.json({ user: { id: req.params.id } });
});
usersRouter.post('/', (req, res) => {
res.created({ user: req.body });
});
module.exports = usersRouter;
// app.js
const app = Lieko();
const usersRouter = require('./routes/users');
// Mount router on /api/users
app.use('/api/users', usersRouter);
// Mount with middleware
app.use('/api/admin', authenticate, requireAdmin, adminRouter);
Error Handling
Centralized error handling with custom error handlers.
Global Error Handler
// Error handler must have 4 parameters
app.errorHandler((err, req, res, next) => {
console.error('Error:', err.message);
// Handle different error types
if (err.name === 'ValidationError') {
return res.status(400).json({
error: {
message: 'Validation failed',
details: err.errors
}
});
}
if (err.code === 'UNAUTHORIZED') {
return res.status(401).json({
error: {
message: 'Authentication required',
code: 'UNAUTHORIZED'
}
});
}
// Default error response
res.status(500).json({
error: {
message: 'Internal server error',
code: 'SERVER_ERROR'
}
});
});
Custom Error Classes
class AppError extends Error {
constructor(message, status, code) {
super(message);
this.status = status;
this.code = code;
this.name = 'AppError';
}
}
class NotFoundError extends AppError {
constructor(message = 'Resource not found') {
super(message, 404, 'NOT_FOUND');
this.name = 'NotFoundError';
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
this.name = 'UnauthorizedError';
}
}
// Usage
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) {
throw new NotFoundError('User not found');
}
res.json({ user });
} catch (error) {
next(error);
}
});
Async Error Handling
// Async errors are automatically caught
app.get('/users', async (req, res) => {
// This error will be caught automatically
const users = await db.users.findAll();
res.json({ users });
});
// Manual error throwing
app.post('/users', async (req, res, next) => {
try {
const existing = await db.users.findByEmail(req.body.email);
if (existing) {
throw new AppError('Email already exists', 409, 'CONFLICT');
}
const user = await db.users.create(req.body);
res.created({ user });
} catch (error) {
next(error);
}
});
404 Handler
// Custom 404 handler (place after all routes)
app.notFound((req, res) => {
res.status(404).json({
error: {
message: 'Route not found',
path: req.url,
method: req.method
}
});
});
File Uploads
Built-in multipart form data handling for file uploads.
Basic Upload
app.post('/upload', (req, res) => {
// Access uploaded files
if (!req.files || !req.files.avatar) {
return res.badRequest('No file uploaded');
}
const file = req.files.avatar;
res.json({
filename: file.filename,
size: file.size,
contentType: file.contentType,
message: 'File uploaded successfully'
});
});
Saving Files
const fs = require('fs').promises;
const path = require('path');
app.post('/upload', async (req, res) => {
const file = req.files.document;
if (!file) {
return res.badRequest('No file provided');
}
// Generate unique filename
const filename = `${Date.now()}-${file.filename}`;
const filepath = path.join(__dirname, 'uploads', filename);
try {
await fs.writeFile(filepath, file.data);
res.created({
filename,
originalName: file.filename,
size: file.size,
path: `/uploads/${filename}`
});
} catch (error) {
res.serverError('Failed to save file');
}
});
Multiple Files
app.post('/upload-multiple', async (req, res) => {
const files = req.files;
if (!files || Object.keys(files).length === 0) {
return res.badRequest('No files uploaded');
}
const uploaded = [];
for (const [field, file] of Object.entries(files)) {
const filename = `${Date.now()}-${file.filename}`;
const filepath = path.join(__dirname, 'uploads', filename);
await fs.writeFile(filepath, file.data);
uploaded.push({
field,
filename,
originalName: file.filename,
size: file.size
});
}
res.created({ files: uploaded });
});
File Validation
const validateFile = (file, options = {}) => {
const {
maxSize = 5 * 1024 * 1024, // 5MB
allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
} = options;
if (file.size > maxSize) {
throw new Error(`File too large. Max size: ${maxSize} bytes`);
}
if (!allowedTypes.includes(file.contentType)) {
throw new Error(`Invalid file type. Allowed: ${allowedTypes.join(', ')}`);
}
return true;
};
app.post('/upload-image', async (req, res) => {
const file = req.files.image;
if (!file) {
return res.badRequest('No image provided');
}
try {
validateFile(file, {
maxSize: 10 * 1024 * 1024, // 10MB
allowedTypes: ['image/jpeg', 'image/png', 'image/webp']
});
// Save file...
res.created({ message: 'Image uploaded' });
} catch (error) {
res.badRequest(error.message);
}
});
Form Data with Files
app.post('/profile', async (req, res) => {
// Access form fields
const { name, bio } = req.body;
// Access uploaded file
const avatar = req.files.avatar;
if (!name) {
return res.badRequest('Name is required');
}
const profile = {
name,
bio: bio || '',
avatar: avatar ? {
filename: avatar.filename,
size: avatar.size
} : null
};
res.created(profile);
});
API Methods Reference
Complete reference of all application and response methods.
Application Methods
| Method | Description | Example |
|---|---|---|
get(path, ...handlers) |
Register GET route | app.get('/users', handler) |
post(path, ...handlers) |
Register POST route | app.post('/users', handler) |
put(path, ...handlers) |
Register PUT route | app.put('/users/:id', handler) |
patch(path, ...handlers) |
Register PATCH route | app.patch('/users/:id', handler) |
delete(path, ...handlers) |
Register DELETE route | app.delete('/users/:id', handler) |
all(path, ...handlers) |
Handle all HTTP methods | app.all('/admin', handler) |
use(...middleware) |
Apply middleware | app.use(logger) |
group(path, ...args) |
Create route group | app.group('/api', callback) |
set(name, value) |
Set application setting | app.set('views', './templates') |
enable(name) |
Enable setting | app.enable('trust proxy') |
disable(name) |
Disable setting | app.disable('x-powered-by') |
cors(options) |
Configure CORS | app.cors({ origin: '*' }) |
debug(value) |
Enable debug mode | app.debug(true) |
static(root, options) |
Serve static files | app.use(app.static('public')) |
notFound(handler) |
Custom 404 handler | app.notFound(handler) |
errorHandler(handler) |
Error handling middleware | app.errorHandler(handler) |
listen(port, callback) |
Start server | app.listen(3000) |
listRoutes() |
Get registered routes | const routes = app.listRoutes() |
printRoutes() |
Print routes to console | app.printRoutes() |
Response Methods
| Method | Description | Example |
|---|---|---|
json(data) |
Send JSON response | res.json({ data }) |
send(data) |
Send any data type | res.send('Hello') |
html(html) |
Send HTML response | res.html(' |
status(code) |
Set status code | res.status(404) |
ok(data, message) |
200 success response | res.ok({ users }) |
created(data, message) |
201 created response | res.created({ user }) |
noContent() |
204 no content | res.noContent() |
accepted(data, message) |
202 accepted response | res.accepted() |
paginated(items, total, msg) |
Paginated response | res.paginated(users, 100) |
badRequest(message) |
400 error | res.badRequest('Invalid input') |
unauthorized(message) |
401 error | res.unauthorized() |
forbidden(message) |
403 error | res.forbidden() |
notFound(message) |
404 error | res.notFound() |
serverError(message) |
500 error | res.serverError() |
error(obj) |
Custom error response | res.error({ code: 'ERR' }) |
redirect(url, status) |
Redirect to URL | res.redirect('/home') |
sendFile(path, options) |
Send file | res.sendFile('./file.pdf') |
render(view, data, cb) |
Render template | res.render('index', { title }) |
cookie(name, value, opts) |
Set cookie | res.cookie('session', 'abc') |
clearCookie(name, opts) |
Clear cookie | res.clearCookie('session') |
setHeader(name, value) |
Set response header | res.setHeader('X-Custom', 'value') |
type(mime) |
Set content type | res.type('text/html') |
end(data) |
End response | res.end() |
Validators Reference
Complete list of built-in validation functions.
const { validators: v } = require('lieko-express');
// Type validators
v.string(message) // Must be string
v.number(message) // Must be number
v.boolean(message) // Must be boolean
v.integer(message) // Must be integer
v.date(message) // Must be valid date
// Presence validators
v.required(message) // Field is required
v.optional() // Field is optional
v.requiredTrue(message) // Must be true (checkbox)
// Number validators
v.positive(message) // Must be > 0
v.negative(message) // Must be < 0
v.min(value, message) // Minimum value/length
v.max(value, message) // Maximum value/length
// String validators
v.minLength(n, message) // Minimum string length
v.maxLength(n, message) // Maximum string length
v.length(n, message) // Exact string length
v.email(message) // Valid email format
v.pattern(regex, message) // Match regex pattern
v.startsWith(str, message) // String starts with
v.endsWith(str, message) // String ends with
// Value validators
v.oneOf(values, message) // Value in array
v.notOneOf(values, message) // Value not in array
v.equal(value, message) // Value equals
v.mustBeTrue(message) // Must be true
v.mustBeFalse(message) // Must be false
// Date validators
v.before(date, message) // Date before
v.after(date, message) // Date after
// Custom validator
v.custom((value, data) => {
// Return true if valid, false if invalid
return value !== 'reserved';
}, message);
Best Practices
Recommended patterns and practices for building applications.
Project Structure
my-app/
├── src/
│ ├── routes/
│ │ ├── users.js
│ │ ├── posts.js
│ │ └── auth.js
│ ├── middleware/
│ │ ├── auth.js
│ │ ├── validate.js
│ │ └── logger.js
│ ├── controllers/
│ │ ├── userController.js
│ │ └── postController.js
│ ├── models/
│ │ ├── User.js
│ │ └── Post.js
│ ├── schemas/
│ │ ├── userSchema.js
│ │ └── postSchema.js
│ ├── utils/
│ │ ├── errors.js
│ │ └── helpers.js
│ └── app.js
├── public/
│ ├── css/
│ ├── js/
│ └── images/
├── views/
│ └── index.html
├── .env
└── package.json
Environment Variables
// Load environment variables
require('dotenv').config();
const app = Lieko();
// Use environment variables
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || 'development';
// Configure based on environment
if (NODE_ENV === 'development') {
app.debug(true);
}
if (NODE_ENV === 'production') {
app.enable('trust proxy');
app.cors({
origin: process.env.ALLOWED_ORIGINS.split(','),
credentials: true
});
}
app.listen(PORT, () => {
console.log(`Server running on port ${PORT} in ${NODE_ENV} mode`);
});
Modular Routes
// routes/users.js
const Lieko = require('lieko-express');
const router = Lieko.Router();
const { authenticate } = require('../middleware/auth');
const userController = require('../controllers/userController');
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.post('/', authenticate, userController.create);
router.patch('/:id', authenticate, userController.update);
router.delete('/:id', authenticate, userController.delete);
module.exports = router;
// app.js
const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);
Error Handling Pattern
// utils/errors.js
class AppError extends Error {
constructor(message, status = 500, code = 'SERVER_ERROR') {
super(message);
this.status = status;
this.code = code;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed', 400, 'VALIDATION_ERROR');
this.errors = errors;
}
}
module.exports = { AppError, ValidationError };
// app.js
const { AppError } = require('./utils/errors');
app.errorHandler((err, req, res, next) => {
console.error(err);
if (err instanceof AppError) {
return res.status(err.status).json({
error: {
message: err.message,
code: err.code,
...(err.errors && { errors: err.errors })
}
});
}
res.status(500).json({
error: {
message: 'Internal Server Error',
code: 'SERVER_ERROR'
}
});
});
Security Headers
// Security middleware
app.use((req, res, next) => {
// Content Security Policy
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'"
);
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// XSS protection
res.setHeader('X-Content-Type-Options', 'nosniff');
// HTTPS enforcement
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
next();
});
Request Logging
// middleware/logger.js
const logger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ` +
`${req.method} ${req.url} ` +
`${res.statusCode} - ${duration}ms`
);
});
next();
};
module.exports = logger;
// app.js
app.use(logger);
Rate Limiting
// Simple in-memory rate limiter
const rateLimit = (options = {}) => {
const {
windowMs = 15 * 60 * 1000, // 15 minutes
max = 100 // Max requests per window
} = options;
const requests = new Map();
return (req, res, next) => {
const key = req.ip.display;
const now = Date.now();
if (!requests.has(key)) {
requests.set(key, []);
}
const userRequests = requests.get(key);
// Remove old requests
const validRequests = userRequests.filter(
time => now - time < windowMs
);
if (validRequests.length >= max) {
return res.status(429).json({
error: {
message: 'Too many requests',
code: 'RATE_LIMIT_EXCEEDED'
}
});
}
validRequests.push(now);
requests.set(key, validRequests);
next();
};
};
// Apply to routes
app.use('/api/', rateLimit({ max: 100 }));
Example: Basic REST API
A complete REST API with CRUD operations.
const Lieko = require('lieko-express');
const { Schema, validators: v, validate } = require('lieko-express');
const app = Lieko();
app.debug(true);
// In-memory database
const todos = [];
let idCounter = 1;
// Validation schemas
const createTodoSchema = new Schema({
title: [v.required(), v.string(), v.minLength(3)],
description: [v.optional(), v.string()],
completed: [v.optional(), v.boolean()]
});
const updateTodoSchema = new Schema({
title: [v.optional(), v.string(), v.minLength(3)],
description: [v.optional(), v.string()],
completed: [v.optional(), v.boolean()]
});
// Routes
app.get('/todos', (req, res) => {
const { completed } = req.query;
let filtered = todos;
if (completed !== undefined) {
filtered = todos.filter(t => t.completed === completed);
}
res.ok(filtered, 'Todos retrieved successfully');
});
app.get('/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === req.params.id);
if (!todo) {
return res.notFound('Todo not found');
}
res.ok(todo);
});
app.post('/todos', validate(createTodoSchema), (req, res) => {
const todo = {
id: String(idCounter++),
title: req.body.title,
description: req.body.description || '',
completed: req.body.completed || false,
createdAt: new Date().toISOString()
};
todos.push(todo);
res.created(todo, 'Todo created successfully');
});
app.patch('/todos/:id', validate(updateTodoSchema), (req, res) => {
const todo = todos.find(t => t.id === req.params.id);
if (!todo) {
return res.notFound('Todo not found');
}
Object.assign(todo, req.body);
todo.updatedAt = new Date().toISOString();
res.ok(todo, 'Todo updated successfully');
});
app.delete('/todos/:id', (req, res) => {
const index = todos.findIndex(t => t.id === req.params.id);
if (index === -1) {
return res.notFound('Todo not found');
}
todos.splice(index, 1);
res.noContent();
});
// Error handler
app.errorHandler((err, req, res, next) => {
console.error(err);
res.status(500).json({
error: {
message: 'Internal Server Error',
code: 'SERVER_ERROR'
}
});
});
app.listen(3000, () => {
console.log('Todo API running on http://localhost:3000');
});
Example: Authentication
JWT-based authentication system.
const Lieko = require('lieko-express');
const { Schema, validators: v, validate } = require('lieko-express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = Lieko();
const SECRET_KEY = 'your-secret-key';
// In-memory users database
const users = [];
// Schemas
const registerSchema = new Schema({
username: [v.required(), v.string(), v.minLength(3)],
email: [v.required(), v.email()],
password: [v.required(), v.minLength(8)]
});
const loginSchema = new Schema({
email: [v.required(), v.email()],
password: [v.required()]
});
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.bearer;
if (!token) {
return res.unauthorized('No token provided');
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
res.unauthorized('Invalid token');
}
};
// Routes
app.post('/register', validate(registerSchema), async (req, res) => {
const { username, email, password } = req.body;
// Check if user exists
if (users.find(u => u.email === email)) {
return res.error({
message: 'Email already registered',
code: 'EMAIL_EXISTS',
status: 409
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
const user = {
id: String(users.length + 1),
username,
email,
password: hashedPassword,
createdAt: new Date().toISOString()
};
users.push(user);
// Generate token
const token = jwt.sign(
{ id: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '24h' }
);
res.created({
user: { id: user.id, username, email },
token
}, 'User registered successfully');
});
app.post('/login', validate(loginSchema), async (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user) {
return res.unauthorized('Invalid credentials');
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.unauthorized('Invalid credentials');
}
const token = jwt.sign(
{ id: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '24h' }
);
res.ok({
user: { id: user.id, username: user.username, email },
token
}, 'Login successful');
});
app.get('/profile', authenticate, (req, res) => {
const user = users.find(u => u.id === req.user.id);
if (!user) {
return res.notFound('User not found');
}
res.ok({
id: user.id,
username: user.username,
email: user.email,
createdAt: user.createdAt
});
});
app.patch('/profile', authenticate, async (req, res) => {
const user = users.find(u => u.id === req.user.id);
if (!user) {
return res.notFound('User not found');
}
const { username, password } = req.body;
if (username) user.username = username;
if (password) {
user.password = await bcrypt.hash(password, 10);
}
res.ok({
id: user.id,
username: user.username,
email: user.email
}, 'Profile updated successfully');
});
app.listen(3000, () => {
console.log('Auth API running on http://localhost:3000');
});
Example: File Upload Service
Complete file upload service with validation.
const Lieko = require('lieko-express');
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const app = Lieko();
const UPLOAD_DIR = path.join(__dirname, 'uploads');
// Ensure upload directory exists
fs.mkdir(UPLOAD_DIR, { recursive: true });
// File validation
const validateFile = (file, options = {}) => {
const {
maxSize = 10 * 1024 * 1024, // 10MB
allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
} = options;
const errors = [];
if (file.size > maxSize) {
errors.push(`File too large. Max size: ${maxSize / 1024 / 1024}MB`);
}
if (!allowedTypes.includes(file.contentType)) {
errors.push(`Invalid file type. Allowed: ${allowedTypes.join(', ')}`);
}
return errors;
};
// Generate unique filename
const generateFilename = (originalName) => {
const ext = path.extname(originalName);
const hash = crypto.randomBytes(16).toString('hex');
return `${Date.now()}-${hash}${ext}`;
};
// Upload single file
app.post('/upload', async (req, res) => {
if (!req.files || !req.files.file) {
return res.badRequest('No file uploaded');
}
const file = req.files.file;
// Validate file
const errors = validateFile(file);
if (errors.length > 0) {
return res.badRequest(errors.join(', '));
}
try {
const filename = generateFilename(file.filename);
const filepath = path.join(UPLOAD_DIR, filename);
await fs.writeFile(filepath, file.data);
res.created({
filename,
originalName: file.filename,
size: file.size,
contentType: file.contentType,
url: `/files/${filename}`
}, 'File uploaded successfully');
} catch (error) {
console.error('Upload error:', error);
res.serverError('Failed to upload file');
}
});
// Upload multiple files
app.post('/upload-multiple', async (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.badRequest('No files uploaded');
}
const uploadedFiles = [];
const errors = [];
for (const [field, file] of Object.entries(req.files)) {
const validationErrors = validateFile(file);
if (validationErrors.length > 0) {
errors.push({ field, errors: validationErrors });
continue;
}
try {
const filename = generateFilename(file.filename);
const filepath = path.join(UPLOAD_DIR, filename);
await fs.writeFile(filepath, file.data);
uploadedFiles.push({
field,
filename,
originalName: file.filename,
size: file.size,
contentType: file.contentType,
url: `/files/${filename}`
});
} catch (error) {
errors.push({ field, errors: ['Upload failed'] });
}
}
if (uploadedFiles.length === 0) {
return res.badRequest('No files were uploaded successfully');
}
res.created({
files: uploadedFiles,
...(errors.length > 0 && { errors })
}, `${uploadedFiles.length} file(s) uploaded successfully`);
});
// Serve uploaded files
app.use('/files', app.static(UPLOAD_DIR));
// Get file info
app.get('/files/:filename/info', async (req, res) => {
const filepath = path.join(UPLOAD_DIR, req.params.filename);
try {
const stats = await fs.stat(filepath);
res.ok({
filename: req.params.filename,
size: stats.size,
created: stats.birthtime,
modified: stats.mtime
});
} catch (error) {
res.notFound('File not found');
}
});
// Delete file
app.delete('/files/:filename', async (req, res) => {
const filepath = path.join(UPLOAD_DIR, req.params.filename);
try {
await fs.unlink(filepath);
res.noContent();
} catch (error) {
res.notFound('File not found');
}
});
app.listen(3000, () => {
console.log('Upload service running on http://localhost:3000');
});
Example: Real-world Application
A complete blog API with authentication, validation, and error handling.
const Lieko = require('lieko-express');
const { Schema, validators: v, validate } = require('lieko-express');
const jwt = require('jsonwebtoken');
const app = Lieko();
app.debug(true);
const SECRET = 'your-secret-key';
// Database
const users = [];
const posts = [];
let userIdCounter = 1;
let postIdCounter = 1;
// Middleware
const authenticate = (req, res, next) => {
const token = req.bearer;
if (!token) {
return res.unauthorized('Authentication required');
}
try {
const decoded = jwt.verify(token, SECRET);
req.user = users.find(u => u.id === decoded.id);
if (!req.user) throw new Error('User not found');
next();
} catch (error) {
res.unauthorized('Invalid token');
}
};
const isAuthor = (req, res, next) => {
const post = posts.find(p => p.id === req.params.id);
if (!post) {
return res.notFound('Post not found');
}
if (post.authorId !== req.user.id) {
return res.forbidden('Not authorized to modify this post');
}
next();
};
// Schemas
const registerSchema = new Schema({
username: [v.required(), v.string(), v.minLength(3)],
email: [v.required(), v.email()],
password: [v.required(), v.minLength(8)]
});
const loginSchema = new Schema({
email: [v.required(), v.email()],
password: [v.required()]
});
const postSchema = new Schema({
title: [v.required(), v.string(), v.minLength(5)],
content: [v.required(), v.string(), v.minLength(10)],
tags: [v.optional()]
});
// Auth routes
app.group('/auth', (auth) => {
auth.post('/register', validate(registerSchema), (req, res) => {
const { username, email, password } = req.body;
if (users.find(u => u.email === email)) {
return res.error({
message: 'Email already exists',
code: 'EMAIL_EXISTS',
status: 409
});
}
const user = {
id: String(userIdCounter++),
username,
email,
password, // Should be hashed in production
createdAt: new Date().toISOString()
};
users.push(user);
const token = jwt.sign({ id: user.id }, SECRET, { expiresIn: '7d' });
res.created({
user: { id: user.id, username, email },
token
});
});
auth.post('/login', validate(loginSchema), (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email && u.password === password);
if (!user) {
return res.unauthorized('Invalid credentials');
}
const token = jwt.sign({ id: user.id }, SECRET, { expiresIn: '7d' });
res.ok({
user: { id: user.id, username: user.username, email },
token
});
});
});
// Post routes
app.group('/posts', (postRoutes) => {
// Public routes
postRoutes.get('/', (req, res) => {
const { page = 1, limit = 10, tag } = req.query;
let filtered = posts;
if (tag) {
filtered = posts.filter(p => p.tags?.includes(tag));
}
const total = filtered.length;
const start = (page - 1) * limit;
const paginated = filtered.slice(start, start + limit);
res.paginated(paginated, total);
});
postRoutes.get('/:id', (req, res) => {
const post = posts.find(p => p.id === req.params.id);
if (!post) {
return res.notFound('Post not found');
}
const author = users.find(u => u.id === post.authorId);
res.ok({
...post,
author: {
id: author.id,
username: author.username
}
});
});
// Protected routes
postRoutes.post('/', authenticate, validate(postSchema), (req, res) => {
const post = {
id: String(postIdCounter++),
title: req.body.title,
content: req.body.content,
tags: req.body.tags || [],
authorId: req.user.id,
createdAt: new Date().toISOString()
};
posts.push(post);
res.created(post);
});
postRoutes.patch('/:id', authenticate, isAuthor, validate(postSchema), (req, res) => {
const post = posts.find(p => p.id === req.params.id);
Object.assign(post, req.body);
post.updatedAt = new Date().toISOString();
res.ok(post);
});
postRoutes.delete('/:id', authenticate, isAuthor, (req, res) => {
const index = posts.findIndex(p => p.id === req.params.id);
posts.splice(index, 1);
res.noContent();
});
});
// User routes
app.get('/users/:id/posts', (req, res) => {
const userPosts = posts.filter(p => p.authorId === req.params.id);
res.ok(userPosts);
});
// Error handling
app.errorHandler((err, req, res, next) => {
console.error('Error:', err);
res.status(err.status || 500).json({
error: {
message: err.message || 'Internal Server Error',
code: err.code || 'SERVER_ERROR'
}
});
});
app.listen(3000, () => {
console.log('Blog API running on http://localhost:3000');
app.printRoutes();
});