Package Exports
- nexting
- nexting/client
- nexting/server
Readme
Nexting
A comprehensive, type-safe full-stack library for TypeScript/JavaScript applications. Nexting provides server actions, API controllers, React hooks, error handling, and professional logging - all with complete type safety and inference.
โจ Features
- ๐ฏ Type-Safe Server Actions - Create server functions with automatic input/output validation
- ๐ API Controllers - Build RESTful endpoints with structured error handling
- โ๏ธ React Hooks Integration - SWR-based hooks for seamless server action integration
- ๐ก๏ธ Comprehensive Error Handling - Structured error responses with user-friendly messages
- ๐ Professional Logging System - Multiple formatters, transports, and request tracking
- ๐ Full TypeScript Support - Complete type inference and strict type checking
- ๐ Universal/Isomorphic - Works in both server and client environments
- ๐ฆ Modular Exports - Import only what you need with tree-shaking support
- โ Production Ready - Comprehensive test coverage and battle-tested
๐ฆ Installation
npm install nexting
# or
yarn add nexting
# or
pnpm add nexting
Peer Dependencies:
npm install typescript zod swr
๐จ Environment-Specific Imports
Critical: To avoid runtime errors, use specific imports based on your environment:
๐ฅ๏ธ Server/Backend Usage
// โ
Server-safe imports
import { makeServerAction, makeApiController, createLogger } from 'nexting/server';
// or
import { makeServerAction, ServerError } from 'nexting'; // Main export is server-safe
๐ Client/Frontend Usage
// โ
Client-specific imports
import { makeServerActionImmutableHook, makeServerActionMutationHook } from 'nexting/client';
๐ Universal Usage
// โ
Safe in both environments
import { ServerError, parseServerError, zod } from 'nexting';
๐ Quick Start
1. Create a Type-Safe Server Action
// actions/user-actions.ts
'use server';
import { makeServerAction, zod } from 'nexting/server';
export const createUserAction = makeServerAction(async ({ name, email }) => {
// Fully typed parameters
const user = await db.user.create({
data: { name, email }
});
return {
id: user.id,
name: user.name,
email: user.email
};
}, {
validationSchema: zod.object({
name: zod.string().min(3, 'Name must be at least 3 characters'),
email: zod.string().email('Invalid email format'),
}),
});
// Simple action without validation
export const getServerTimeAction = makeServerAction(async () => {
return {
timestamp: new Date().toISOString(),
message: 'Server is running!'
};
});
2. Use Server Actions in React Components
// components/UserForm.tsx
'use client';
import { makeServerActionMutationHook } from 'nexting/client';
import { createUserAction } from '../actions/user-actions';
const useCreateUser = makeServerActionMutationHook({
key: 'create-user',
action: createUserAction,
});
export function UserForm() {
const { trigger, isMutating, error } = useCreateUser.useAction({
options: {
onSuccess: (user) => {
console.log('User created:', user);
// user is fully typed!
},
onError: (error) => {
console.error('Creation failed:', error);
},
},
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await trigger({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Your name" required />
<input name="email" type="email" placeholder="Your email" required />
<button type="submit" disabled={isMutating}>
{isMutating ? 'Creating...' : 'Create User'}
</button>
{error && <p style={{ color: 'red' }}>{error.message}</p>}
</form>
);
}
3. Create API Controllers for RESTful Endpoints
// api/users/route.ts
import { makeApiController, zod } from 'nexting/server';
// Query parameters are automatically parsed from the URL
const getUsersController = makeApiController(async ({ query }, { request }) => {
const users = await db.user.findMany({
where: query.search ? {
OR: [
{ name: { contains: query.search } },
{ email: { contains: query.search } },
],
} : {},
take: query.limit,
skip: query.offset,
});
return {
data: {
users,
total: users.length,
page: Math.floor(query.offset / query.limit) + 1,
}
};
}, {
querySchema: zod.object({
search: zod.string().optional(),
limit: zod.coerce.number().min(1).max(100).default(10),
offset: zod.coerce.number().min(0).default(0),
}),
});
// No need to manually parse query parameters
export async function GET(request: Request) {
return getUsersController(request);
}
๐ Core Concepts
๐ฏ Server Actions
Server actions are type-safe functions that run on the server with automatic validation and error handling.
Basic Server Action
import { makeServerAction } from 'nexting/server';
const getDataAction = makeServerAction(async () => {
const data = await fetchData();
return { data, timestamp: Date.now() };
});
Server Action with Validation
import { makeServerAction, zod } from 'nexting/server';
const updateProfileAction = makeServerAction(async ({ userId, profile }) => {
const updatedUser = await db.user.update({
where: { id: userId },
data: profile,
});
return updatedUser;
}, {
validationSchema: zod.object({
userId: zod.string().uuid(),
profile: zod.object({
name: zod.string().min(1),
bio: zod.string().max(500).optional(),
}),
}),
});
Error Handling in Server Actions
import { makeServerAction, ServerError } from 'nexting/server';
const deleteUserAction = makeServerAction(async ({ userId }) => {
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) {
throw new ServerError({
message: 'User not found',
code: 'USER_NOT_FOUND',
status: 404,
uiMessage: 'The user you are trying to delete does not exist.',
});
}
await db.user.delete({ where: { id: userId } });
return { success: true };
}, {
validationSchema: zod.object({
userId: zod.string().uuid(),
}),
});
๐ API Controllers
API controllers provide a structured way to create RESTful endpoints with automatic request/response handling and flexible validation for body, query parameters, and route parameters.
Body Validation Only
import { makeApiController, zod } from 'nexting/server';
const createPostController = makeApiController(async ({ body }, { request }) => {
const userId = request.headers.get('user-id');
const post = await db.post.create({
data: {
...body,
authorId: userId,
},
});
return {
post,
message: 'Post created successfully',
};
}, {
bodySchema: zod.object({
title: zod.string().min(1).max(200),
content: zod.string().min(1),
tags: zod.array(zod.string()).optional(),
}),
});
// Use in API route
export async function POST(request: Request) {
return createPostController(request);
}
Query Parameters Validation
const getUsersController = makeApiController(async ({ query }, { request }) => {
const users = await db.user.findMany({
where: query.search ? {
OR: [
{ name: { contains: query.search } },
{ email: { contains: query.search } },
],
} : {},
take: query.limit,
skip: query.offset,
});
return {
users,
total: users.length,
page: Math.floor(query.offset / query.limit) + 1,
};
}, {
querySchema: zod.object({
search: zod.string().optional(),
limit: zod.coerce.number().min(1).max(100).default(10),
offset: zod.coerce.number().min(0).default(0),
}),
});
export async function GET(request: Request) {
return getUsersController(request);
}
Route Parameters Validation
const getUserController = makeApiController(async ({ params }, { request }) => {
const user = await db.user.findUnique({
where: { id: params.userId },
});
if (!user) {
throw new ServerError({
message: 'User not found',
status: 404,
uiMessage: 'The requested user does not exist.',
});
}
return { user };
}, {
paramsSchema: zod.object({
userId: zod.string().uuid(),
}),
});
// app/api/users/[userId]/route.ts
export async function GET(request: Request, { params }: { params: { userId: string } }) {
return getUserController(request, params);
}
Combined Validation (Body + Query + Params)
const updateUserController = makeApiController(async ({ body, query, params }, { request }) => {
// Verify user permissions
if (query.force !== 'true') {
// Check if user has admin privileges
const hasPermission = await checkUserPermissions(request);
if (!hasPermission) {
throw new ServerError({ message: 'Insufficient permissions', status: 403 });
}
}
const updatedUser = await db.user.update({
where: { id: params.userId },
data: body,
});
return {
user: updatedUser,
message: 'User updated successfully',
};
}, {
bodySchema: zod.object({
name: zod.string().min(1).optional(),
email: zod.string().email().optional(),
bio: zod.string().max(500).optional(),
}),
querySchema: zod.object({
force: zod.enum(['true', 'false']).optional(),
}),
paramsSchema: zod.object({
userId: zod.string().uuid(),
}),
});
// app/api/users/[userId]/route.ts
export async function PATCH(request: Request, { params }: { params: { userId: string } }) {
return updateUserController(request, params);
}
โ๏ธ React Hooks
Nexting provides two types of hooks for different use cases:
Mutation Hooks (for Create, Update, Delete operations)
import { makeServerActionMutationHook } from 'nexting/client';
const useCreatePost = makeServerActionMutationHook({
key: 'create-post',
action: createPostAction,
});
function CreatePostForm() {
const { trigger, isMutating, error, data } = useCreatePost.useAction({
options: {
onSuccess: (post) => {
// Redirect or show success message
router.push(`/posts/${post.id}`);
},
onError: (error) => {
toast.error(error.uiMessage || error.message);
},
},
});
const handleSubmit = async (formData: PostFormData) => {
await trigger(formData);
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
<button disabled={isMutating}>
{isMutating ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
Immutable Hooks (for Read operations with caching)
import { makeServerActionImmutableHook } from 'nexting/client';
const useUserProfile = makeServerActionImmutableHook({
key: 'user-profile',
action: getUserProfileAction,
});
function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useUserProfile.useAction({
context: { userId },
options: {
revalidateOnFocus: false,
dedupingInterval: 5000,
},
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
}
๐ก๏ธ Error Handling
Nexting provides a comprehensive error handling system:
ServerError Class
import { ServerError } from 'nexting';
// Create structured errors
const error = new ServerError({
message: 'Database connection failed',
code: 'DB_CONNECTION_ERROR',
status: 500,
uiMessage: 'Something went wrong. Please try again later.',
});
// Errors are automatically serialized
console.log(error.toJSON());
// {
// message: 'Database connection failed',
// code: 'DB_CONNECTION_ERROR',
// status: 500,
// uiMessage: 'Something went wrong. Please try again later.'
// }
Error Parsing and Handling
import { parseServerError } from 'nexting/server';
try {
await riskyOperation();
} catch (error) {
// Automatically converts any error to ServerError format
const serverError = parseServerError(error);
console.log(serverError.toJSON());
}
๐ Professional Logging System
Nexting includes a production-ready logging system with multiple formatters and transports.
Basic Logger Setup
import { createLogger, LogLevel } from 'nexting/server';
const logger = createLogger({
level: LogLevel.INFO,
context: 'API',
});
await logger.info('Server started', { port: 3000 });
await logger.error('Database error', { error: 'Connection timeout' });
Advanced Logger Configuration
import {
createLogger,
LogLevel,
PrettyFormatter,
JsonFormatter,
ConsoleTransport,
FileTransport
} from 'nexting/server';
const logger = createLogger({
level: LogLevel.DEBUG,
context: 'APP',
formatter: process.env.NODE_ENV === 'production'
? new JsonFormatter()
: new PrettyFormatter(),
transports: [
new ConsoleTransport(),
new FileTransport({
filename: './logs/app.log',
maxFileSize: 50 * 1024 * 1024, // 50MB
maxFiles: 10
}),
],
});
Request Logging
import { createRequestLogger } from 'nexting/server';
const requestLogger = createRequestLogger();
export async function middleware(request: Request) {
const requestId = await requestLogger.logRequest(request);
const startTime = Date.now();
try {
const response = await next(request);
const duration = Date.now() - startTime;
await requestLogger.logResponse(requestId, response.status, duration);
return response;
} catch (error) {
await requestLogger.logError(requestId, error);
throw error;
}
}
Child Loggers
const mainLogger = createLogger({ context: 'APP' });
const authLogger = mainLogger.child('AUTH');
const dbLogger = mainLogger.child('DATABASE');
await authLogger.info('User logged in'); // [APP:AUTH] User logged in
await dbLogger.error('Connection lost'); // [APP:DATABASE] Connection lost
๐ง Advanced Usage
Custom Error Handling
import { makeServerAction, parseServerError } from 'nexting/server';
const customAction = makeServerAction(async (data) => {
// Your logic here
return result;
}, {
validationSchema: schema,
error: {
fallbackMessage: 'Custom error occurred',
includeStack: process.env.NODE_ENV === 'development',
},
logger: customLogger,
});
Type Inference Utilities
import type {
InferActionInput,
InferActionOutput,
InferActionError
} from 'nexting/server';
// Extract types from your actions
type CreateUserInput = InferActionInput<typeof createUserAction>;
type CreateUserOutput = InferActionOutput<typeof createUserAction>;
type CreateUserError = InferActionError<typeof createUserAction>;
Environment Configuration
const isDevelopment = process.env.NODE_ENV !== 'production';
const logger = createLogger({
level: isDevelopment ? LogLevel.DEBUG : LogLevel.INFO,
context: 'SERVER',
formatter: isDevelopment ? new PrettyFormatter() : new JsonFormatter(),
transports: isDevelopment
? [new ConsoleTransport()]
: [
new ConsoleTransport(),
new FileTransport({ filename: './logs/app.log' })
]
});
๐ API Reference
Server Actions
function makeServerAction<T, R>(
handler: (input: T) => Promise<R>,
options?: {
validationSchema?: ZodSchema<T>;
error?: ParseServerErrorOptions;
logger?: Logger;
}
): (input: T) => Promise<AsyncState<R, ActionError>>;
API Controllers
// Body + Query + Params validation
function makeApiController<BodySchema, QuerySchema, ParamsSchema, R>(
controller: (
args: {
body: z.infer<BodySchema>;
query: z.infer<QuerySchema>;
params: z.infer<ParamsSchema>
},
ctx: { request: NextRequest }
) => Promise<ApiMakerResponse<R>>,
options: {
bodySchema: BodySchema;
querySchema: QuerySchema;
paramsSchema: ParamsSchema;
error?: ParseServerErrorOptions;
logger?: Logger;
}
): (request: NextRequest, params?: unknown) => Promise<Response>;
// Body only validation
function makeApiController<BodySchema, R>(
controller: (
args: { body: z.infer<BodySchema> },
ctx: { request: NextRequest }
) => Promise<ApiMakerResponse<R>>,
options: {
bodySchema: BodySchema;
error?: ParseServerErrorOptions;
logger?: Logger;
}
): (request: NextRequest, params?: unknown) => Promise<Response>;
// Query only validation
function makeApiController<QuerySchema, R>(
controller: (
args: { query: z.infer<QuerySchema> },
ctx: { request: NextRequest }
) => Promise<ApiMakerResponse<R>>,
options: {
querySchema: QuerySchema;
error?: ParseServerErrorOptions;
logger?: Logger;
}
): (request: NextRequest, params?: unknown) => Promise<Response>;
// Params only validation
function makeApiController<ParamsSchema, R>(
controller: (
args: { params: z.infer<ParamsSchema> },
ctx: { request: NextRequest }
) => Promise<ApiMakerResponse<R>>,
options: {
paramsSchema: ParamsSchema;
error?: ParseServerErrorOptions;
logger?: Logger;
}
): (request: NextRequest, params?: unknown) => Promise<Response>;
// No validation
function makeApiController<R>(
controller: (ctx: { request: NextRequest }) => Promise<ApiMakerResponse<R>>,
options?: {
error?: ParseServerErrorOptions;
logger?: Logger;
}
): (request: NextRequest, params?: unknown) => Promise<Response>;
// ApiMakerResponse Type
interface ApiMakerResponse<T> {
data: T;
status?: StatusCodes;
}
API Response Object (ApiMakerResponse)
All API controllers must return an ApiMakerResponse<T>
object that provides control over both the response data and HTTP status code.
import { makeApiController, ApiMakerResponse } from 'nexting/server';
import { StatusCodes } from 'http-status-codes';
// Basic response with default 200 OK status
const getHealthController = makeApiController(async (): Promise<ApiMakerResponse<{
status: string;
timestamp: string;
}>> => {
return {
data: {
status: 'healthy',
timestamp: new Date().toISOString(),
}
// status defaults to StatusCodes.OK (200)
};
});
// Response with custom status code
const createUserController = makeApiController(async ({ body }): Promise<ApiMakerResponse<{
success: boolean;
user: User;
}>> => {
const user = await createUser(body);
return {
data: {
success: true,
user,
},
status: StatusCodes.CREATED // 201
};
}, {
bodySchema: userSchema,
});
// Conditional response status based on logic
const processDataController = makeApiController(async ({ body }): Promise<ApiMakerResponse<{
message: string;
processed: boolean;
}>> => {
if (body.shouldProcess) {
// Process immediately
await processData(body.data);
return {
data: {
message: 'Data processed successfully',
processed: true,
},
status: StatusCodes.OK // 200
};
} else {
// Queue for later processing
await queueData(body.data);
return {
data: {
message: 'Data queued for processing',
processed: false,
},
status: StatusCodes.ACCEPTED // 202
};
}
}, {
bodySchema: dataSchema,
});
// Different status codes for different operations
const userController = makeApiController(async ({ body, params }): Promise<ApiMakerResponse<{
message: string;
user?: User;
}>> => {
switch (body.operation) {
case 'create':
const user = await createUser(body.userData);
return {
data: { message: 'User created', user },
status: StatusCodes.CREATED // 201
};
case 'update':
const updatedUser = await updateUser(params.id, body.userData);
return {
data: { message: 'User updated', user: updatedUser },
status: StatusCodes.OK // 200
};
case 'delete':
await deleteUser(params.id);
return {
data: { message: 'User deleted' },
status: StatusCodes.NO_CONTENT // 204
};
default:
throw new ServerError({
message: 'Invalid operation',
code: 'INVALID_OPERATION',
status: StatusCodes.BAD_REQUEST,
});
}
}, {
bodySchema: operationSchema,
paramsSchema: paramsSchema,
});
Available Status Codes
You can use any HTTP status code from the http-status-codes
package:
import { StatusCodes } from 'http-status-codes';
// Success responses
StatusCodes.OK // 200 - Default for successful GET/PUT/PATCH
StatusCodes.CREATED // 201 - Resource created (POST)
StatusCodes.ACCEPTED // 202 - Request accepted for processing
StatusCodes.NO_CONTENT // 204 - Successful DELETE
// Client error responses
StatusCodes.BAD_REQUEST // 400 - Invalid request
StatusCodes.UNAUTHORIZED // 401 - Authentication required
StatusCodes.FORBIDDEN // 403 - Access denied
StatusCodes.NOT_FOUND // 404 - Resource not found
StatusCodes.CONFLICT // 409 - Resource conflict
// Server error responses
StatusCodes.INTERNAL_SERVER_ERROR // 500 - Server error
StatusCodes.BAD_GATEWAY // 502 - Bad gateway
StatusCodes.SERVICE_UNAVAILABLE // 503 - Service unavailable
React Hooks
function makeServerActionMutationHook<TAction>(options: {
key: string;
action: TAction;
}): {
useAction: (options?: SWRMutationConfiguration) => SWRMutationResponse;
makeKey: (context?: Record<string, unknown>) => object;
};
function makeServerActionImmutableHook<TAction>(options: {
key: string;
action: TAction;
}): {
useAction: (options: {
context: InferActionInput<TAction>;
skip?: boolean;
options?: SWRConfiguration;
}) => SWRResponse;
makeKey: (context?: InferActionInput<TAction>) => object;
};
๐งช Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run docs development server
npm run test:docs
๐๏ธ Development Setup
# Clone the repository
git clone https://github.com/rrios-dev/nexting.git
cd nexting
# Install dependencies
npm install
# Build the library
npm run build
# Start development mode
npm run dev
# Run documentation site
npm run test:docs
๐ค Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments
- Built with TypeScript for complete type safety
- Inspired by modern full-stack development patterns
- Powered by Zod for runtime validation
- Integrated with SWR for optimal data fetching
- Designed for scalable production applications
Made with โค๏ธ by Roberto Rรญos