@themarblejar/stark-fetch
v0.1.6
Published
A modern, flexible data fetching library for JavaScript/TypeScript applications with a clean, fluent API.
Readme
Stark Fetch
A modern, flexible data fetching library for JavaScript/TypeScript applications with a clean, fluent API.
Overview
Stark Fetch provides a simple yet powerful way to interact with your backend data. It offers:
- A fluent, chainable API for building queries
- Type-safe operations with TypeScript support
- Flexible data fetching with filtering, sorting, and pagination
- Support for relational data
- Express adapter for easy API creation
Architecture
Stark Fetch follows a clean, layered architecture with a clear separation of concerns:
┌─────────────────┐
│ │
│ Core Module │
│ │
└─────────────────┘
▲
│
│
┌───────────────┼───────────────┐
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Database │ │ Adapters │ │ Utils │
│ Module │ │ Module │ │ Module │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘Structural Flow Diagram
When a client method is called, the following flow occurs:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ │ │ │ │ │ │ │
│ Application│ │ StarkClient │ │ QueryBuilder│ │DatabaseClient│
│ Code │────▶│ (Core) │────▶│ (Core) │────▶│ (Core) │
│ │ │ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ │ │ │
│ DataService │◀────────────────────────│ Knex │
│ (Database) │ │ (Database) │
│ │ │ │
└─────────────┘ └─────────────┘Flow Explanation:
- Application Code calls methods on the StarkClient (e.g.,
client.table('users').getMany()) - StarkClient uses the QueryBuilder to construct the query parameters
- QueryBuilder builds and stores query parameters (table, filter, sort, etc.)
- StarkClient calls the appropriate method on DataService
- DataService delegates to DatabaseClient for CRUD operations
- DatabaseClient executes the database operations using Knex
- Knex performs the actual SQL queries
- Results flow back up the chain to the Application Code
This architecture ensures:
- Clear separation of concerns
- Single responsibility for each component
- Centralized database logic in the DatabaseClient
- Simplified service layer that focuses on business logic
- Consistent error handling throughout the stack
Detailed Sequence Diagram
Here's a detailed sequence diagram showing what happens when client.table('users').filter('role=admin').getMany() is called:
┌───────────┐ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ ┌─────┐
│Application│ │StarkClient │ │QueryBuilder │ │ DataService │ │DatabaseClient │Knex │
│ │ │ │ │ │ │ │ │ │ │ │
└─────┬─────┘ └──────┬─────┘ └──────┬──────┘ └──────┬───────┘ └──────┬─────┘ └──┬──┘
│ │ │ │ │ │
│ table('users') │ │ │ │ │
│──────────────────────▶│ │ │ │ │
│ │ table('users') │ │ │ │
│ │──────────────────────▶│ │ │ │
│ │ │ │ │ │
│ filter('role=admin') │ │ │ │ │
│──────────────────────▶│ │ │ │ │
│ │ filter('role=admin') │ │ │ │
│ │──────────────────────▶│ │ │ │
│ │ │ │ │ │
│ getMany() │ │ │ │ │
│──────────────────────▶│ │ │ │ │
│ │ getTableName() │ │ │ │
│ │──────────────────────▶│ │ │ │
│ │ 'users' │ │ │ │
│ │◀──────────────────────│ │ │ │
│ │ │ │ │ │
│ │ getFilter() │ │ │ │
│ │──────────────────────▶│ │ │ │
│ │ 'role=admin' │ │ │ │
│ │◀──────────────────────│ │ │ │
│ │ │ │ │ │
│ │ getMany('users', {...}) │ │ │
│ │───────────────────────────────────────────────▶│ │ │
│ │ │ │ getMany('users', {...}) │ │
│ │ │ │─────────────────────────▶ │
│ │ │ │ │ from('users') │
│ │ │ │ │───────────────────▶│
│ │ │ │ │ │
│ │ │ │ │ where('role=admin')│
│ │ │ │ │───────────────────▶│
│ │ │ │ │ │
│ │ │ │ │ SQL query executed │
│ │ │ │ │◀───────────────────│
│ │ │ │ │ │
│ │ │ │ │ Format response │
│ │ │ │ │─────┐ │
│ │ │ │ │◀────┘ │
│ │ │ │ Response │ │
│ │ │ │◀───────────────────────── │
│ │ Response │ │ │ │
│ │◀───────────────────────────────────────────────│ │ │
│ │ │ │ │ │
│ │ reset() │ │ │ │
│ │──────────────────────▶│ │ │ │
│ │ │ │ │ │
│ Response │ │ │ │ │
│◀──────────────────────│ │ │ │ │
│ │ │ │ │ │
┌─────┴─────┐ ┌──────┴─────┐ ┌──────┴──────┐ ┌──────┴───────┐ ┌──────┴─────┐ ┌──┴──┐
│Application│ │StarkClient │ │QueryBuilder │ │ DataService │ │DatabaseClient │Knex │
│ │ │ │ │ │ │ │ │ │ │ │
└───────────┘ └────────────┘ └─────────────┘ └──────────────┘ └────────────┘ └─────┘This sequence diagram illustrates:
- How method chaining works with the QueryBuilder
- How the StarkClient retrieves query parameters from the QueryBuilder
- How the DataService delegates to the DatabaseClient
- How the DatabaseClient uses Knex to execute the query
- How the response flows back through the layers
Core Components
Core Module: Contains the essential interfaces, types, and implementations
- StarkClient: Main entry point for application code, provides a fluent API
- QueryBuilder: Builds and manages query parameters
- DatabaseClient: Handles all database operations and CRUD functionality
- Types: Core type definitions
Database Module: Provides service layer and data access
- DataService: Delegates to DatabaseClient for CRUD operations
- Auth Service: Handles authentication operations
Adapters Module: Provides integration with frameworks
- ExpressAdapter: Creates REST APIs with Express
Utils Module: Provides utility functions
- ErrorHandling: Error classes and utilities
- Validation: Input validation utilities
Installation
npm install stark-fetch
# or
yarn add stark-fetchColumn Naming Conventions
Stark Fetch handles the conversion between different naming conventions:
Application Code: When using the Stark Fetch API, you should use lowerCamelCase for all column names (e.g.,
firstName,createdOn,updatedOn).Database Storage: In the database, all column names are stored in lowercase (e.g.,
firstname,createdon,updatedon).Automatic Columns: Every table automatically has three standard columns:
id(primary key)createdOn(creation timestamp)updatedOn(last update timestamp)
Stark Fetch handles the conversion between these formats automatically, so you can always use the lowerCamelCase format in your application code, regardless of how the data is stored in the database.
// Example using lowerCamelCase column names
const result = await client
.table('users')
.filter('createdOn > "2023-01-01"')
.sort('lastName', 'asc')
.select('id', 'firstName', 'lastName', 'email', 'createdOn')
.getMany();Basic Usage
import { createClient } from 'stark-fetch';
// Create a client with database configuration
const client = createClient({
client: 'pg',
connection: {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'myapp'
},
pool: { min: 0, max: 10 }
});
// Fetch users with filtering and pagination
async function getUsers() {
try {
const result = await client
.table('users')
.filter('role=admin')
.sort('createdOn', 'desc')
.paginate(1, 10)
.getMany();
console.log(`Found ${result.meta.total} users`);
return result.data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
// Get a single user by ID with relations
async function getUser(id: string) {
try {
const result = await client
.table('users')
.withRelations(['posts', 'comments'])
.select('id', 'firstName', 'lastName', 'email', 'role')
.getOne(id);
if (!result.data) {
throw new Error(`User with ID ${id} not found`);
}
return result.data;
} catch (error) {
console.error(`Error fetching user ${id}:`, error);
throw error;
}
}
// Create a new user
async function createUser(userData: {
firstName: string;
lastName: string;
email: string;
role: string;
}) {
try {
const result = await client
.table('users')
.create(userData);
console.log(`Created user with ID: ${result.data.id}`);
return result.data;
} catch (error) {
console.error('Error creating user:', error);
throw error;
}
}
// Update a user
async function updateUser(id: string, userData: Partial<{
firstName: string;
lastName: string;
email: string;
role: string;
}>) {
try {
const result = await client
.table('users')
.update(id, userData);
console.log(`Updated user with ID: ${id}`);
return result.data;
} catch (error) {
console.error(`Error updating user ${id}:`, error);
throw error;
}
}
// Delete a user
async function deleteUser(id: string) {
try {
await client
.table('users')
.delete(id);
console.log(`Deleted user with ID: ${id}`);
return true;
} catch (error) {
console.error(`Error deleting user ${id}:`, error);
throw error;
}
}Express API Integration
Stark Fetch makes it easy to create REST APIs with Express:
import express from 'express';
import { createDataRouter, createAuthRouter } from 'stark-fetch/database/adapters';
import { createClient } from 'stark-fetch';
const app = express();
app.use(express.json());
// Create a database configuration
const dbConfig = {
client: 'pg',
connection: {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'myapp'
}
};
// Create a Stark client
const client = createClient(dbConfig);
// Get the database service
const dataService = client.getDatabaseClient().getService();
// Optional middleware for authentication
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
// Verify token and set user on request
req.user = { id: 'user-id', role: 'admin' };
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
// Optional middleware for metrics
const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
});
next();
};
// Create and use data router with middleware
const dataRouter = createDataRouter(dataService, {
authMiddleware: [authMiddleware],
metricsMiddleware
});
app.use('/api', dataRouter);
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err);
if (err.statusCode) {
return res.status(err.statusCode).json({ error: err.message });
}
return res.status(500).json({ error: 'Internal server error' });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});This creates the following REST API endpoints:
GET /api/:tableName- Get multiple recordsGET /api/:tableName/:id- Get a single record by IDPOST /api/:tableName- Create a new recordPUT /api/:tableName/:id- Update a record by IDDELETE /api/:tableName/:id- Delete a record by ID
Advanced Features
Query Building
Stark Fetch now uses a powerful SQL query builder that provides fine-grained control over your database queries:
// Complex filtering with multiple conditions
client
.table('users')
.filter('age>30 AND (role=admin OR role=editor)')
.getMany();
// Multiple sorting with different directions
// Use - prefix for descending order, + or no prefix for ascending
client
.table('users')
.sort('-lastName,firstName,+createdOn')
.getMany();
// Select specific columns for efficiency
client
.table('users')
.select('id', 'firstName', 'lastName', 'email', 'role')
.getMany();
// Load nested relations
client
.table('posts')
.withRelations(['author', 'comments', 'comments.author'])
.getMany();
// Combining multiple query features
client
.table('users')
.filter('isActive=true')
.sort('-createdOn')
.paginate(2, 25)
.select('id', 'firstName', 'lastName', 'email')
.withRelation('posts')
.getMany();The new query builder generates optimized SQL queries with features like:
WITHclauses for complex queriesLEFT JOIN LATERALfor efficient relation loading- Proper quoting of table and column names
- Automatic handling of pagination with offset/limit
- Support for complex sorting with multiple columns and directions
- Proper handling of column name conventions (lowerCamelCase in code, lowercase in database)
Direct Database Access
For more advanced use cases, you can access the DatabaseClient directly:
import { createClient } from 'stark-fetch';
const client = createClient({
client: 'pg',
connection: {
host: 'localhost',
user: 'postgres',
password: 'password',
database: 'myapp'
}
});
// Get the DatabaseClient instance
const dbClient = client.getDatabaseClient();
// Get a user by ID with options
const user = await dbClient.getOne('users', '123', {
withRelation: ['posts'],
sort: 'createdAt',
filter: 'status = "active"'
});Recent Changes
Architecture Improvements
- Centralized Database Logic: All CRUD operations now reside in the DatabaseClient in the core module
- Simplified Service Layer: DataService now delegates to DatabaseClient for all operations
- Clear Separation of Concerns: Core handles database operations, services handle business logic
- Improved Type Safety: Consistent type definitions across all modules
- Modular Structure: Organized into core, database, adapters, and utils modules
Code Improvements
- Reduced Duplication: Database logic is now in one place
- Consistent Error Handling: Standardized error handling across all operations
- Better Testability: Clear separation makes unit testing easier
- Simplified Maintenance: Changes to database logic only need to be made in one place
- Improved Performance: Direct database access without unnecessary abstraction layers
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT
