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
Note: Lieko-express is designed to be a drop-in replacement for Express.js with additional features and better performance.

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
});
Important: Always call 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
Note: If a request exceeds the size limit, a 413 error is automatically returned.

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
    }
  });
});

Cookies

Built-in cookie support with secure defaults.

Setting Cookies

app.get('/login', (req, res) => {
  // Basic cookie
  res.cookie('session', 'abc123');
  
  // Cookie with options
  res.cookie('user', 'alice', {
    maxAge: 86400000,  // 24 hours
    httpOnly: true,    // Not accessible via JavaScript
    secure: true,      // HTTPS only
    sameSite: 'strict' // CSRF protection
  });
  
  res.json({ message: 'Logged in' });
});

Cookie Options

Option Type Default Description
maxAge number null Cookie lifetime in milliseconds
expires Date null Expiration date
path string '/' Cookie path
domain string null Cookie domain
httpOnly boolean true Not accessible via JavaScript
secure boolean false HTTPS only
sameSite string 'lax' 'strict', 'lax', or 'none'

Clearing Cookies

app.get('/logout', (req, res) => {
  // Clear cookie
  res.clearCookie('session');
  
  // Clear with options (must match original cookie)
  res.clearCookie('user', {
    path: '/',
    domain: 'example.com'
  });
  
  res.json({ message: 'Logged out' });
});

Reading Cookies

// Manual cookie parsing
const parseCookies = (cookieHeader) => {
  const cookies = {};
  if (cookieHeader) {
    cookieHeader.split(';').forEach(cookie => {
      const [name, value] = cookie.trim().split('=');
      cookies[name] = decodeURIComponent(value);
    });
  }
  return cookies;
};

app.get('/profile', (req, res) => {
  const cookies = parseCookies(req.headers.cookie);
  const sessionId = cookies.session;
  
  if (!sessionId) {
    return res.unauthorized('No session');
  }
  
  res.json({ session: sessionId });
});

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('

Hello

')
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();
});