@sourceregistry/sveltekit-service-manager
v2.0.6
Published
A minimal, production-oriented service gateway for SvelteKit.
Maintainers
Readme
@sourceregistry/sveltekit-service-manager
A small service gateway, router, and client for SvelteKit backends
Expose modular backend services through versioned SvelteKit gateway routes. Keep service routing, lifecycle, internal calls, browser calls, and optional Node middleware adapters in one small package.
Installation
npm install @sourceregistry/sveltekit-service-managerPeer dependency: svelte ^5.0.0
In this repository examples may import from $lib/server/index.js. In applications, import from @sourceregistry/sveltekit-service-manager.
Overview
// src/routes/api/v1/services/[service_name]/[...catch]/+server.ts
import { ServiceManager } from '@sourceregistry/sveltekit-service-manager';
const { endpoint, access } = ServiceManager.Base(undefined, {
accessKey: 'api:v1',
});
export const { GET, POST, PUT, DELETE, PATCH, HEAD } = endpoint;
access('ping', 'users');// src/lib/server/services/ping.service.ts
import { Action, Router, ServiceManager, type Service } from '@sourceregistry/sveltekit-service-manager';
const router = Router()
.GET('/health', () => Action.success(200, { ok: true }))
.GET('/echo/[message]', ({ params }) => Action.success(200, { message: params.message }));
const service = {
name: 'ping',
route: router,
} satisfies Service<'ping'>;
export default ServiceManager.Load(service, import.meta);This exposes:
/api/v1/services/ping/health
/api/v1/services/ping/echo/helloCore API
Import server utilities from @sourceregistry/sveltekit-service-manager or @sourceregistry/sveltekit-service-manager/server.
ServiceManager.Base
Creates SvelteKit request handlers for a gateway route. The default selector reads event.params.service_name, which matches [service_name].
import { ServiceManager } from '@sourceregistry/sveltekit-service-manager';
const { endpoint, access } = ServiceManager.Base(undefined, {
accessKey: 'public',
});
export const { GET, POST, PUT, DELETE, PATCH, HEAD } = endpoint;
access('ping', 'status');Use a stable accessKey for each gateway. Allow-lists are stored on the singleton service manager so they survive Vite HMR recreations.
Requests for unknown services and blocked services both fail as inaccessible. This avoids exposing which service names are registered.
ServiceManager.Load
Registers a service definition and wires Vite HMR cleanup when import.meta is passed.
import { ServiceManager, Router } from '@sourceregistry/sveltekit-service-manager';
const service = {
name: 'users',
route: Router().GET('/me', ({ locals }) => Response.json({ user: locals.user })),
cleanup: async () => {
// close timers, workers, sockets, or pools owned by this service
},
};
export default ServiceManager.Load(service, import.meta);During HMR, cleanup() runs, router routes are reset, the old service is unregistered, and the updated module can register fresh handlers.
Router
Creates a service-relative router. Routes use SvelteKit-style segments: static paths, [param], and [...catchAll].
import { Action, Router } from '@sourceregistry/sveltekit-service-manager';
export const router = Router()
.GET('/health', () => Action.success(200, { ok: true }))
.POST('/users/[id]', ({ params }) => Action.success(200, { updated: params.id }))
.GET('/files/[...path]', ({ params }) => Action.success(200, { path: params.path }));Supported methods: GET, PUT, POST, DELETE, HEAD, PATCH, OPTIONS. USE(path, handler, methods?) registers one handler for multiple methods.
Nested routers
const users = Router().GET('/profile', ({ params }) => Action.success(200, { userId: params.id }));
const api = Router().use('/users/[id]', users);/users/42/profile reaches the nested router with params.id === '42'.
Pre and post hooks
const router = Router()
.pre((event) => {
const token = event.cookies.get('token');
if (!token) return Action.error(401, { message: 'Unauthorized' } as any);
return {
...event,
locals: { ...event.locals, token },
} as any;
})
.GET('/private', ({ locals }) => Action.success(200, { token: (locals as any).token }))
.post((_event, response) => {
const headers = new Headers(response.headers);
headers.set('x-service-router', '1');
return new Response(response.body, { status: response.status, headers });
});Keep hooks small. Use pre for auth, tenant, actor, tracing, and maintenance stops. Use post for response metadata and shaping.
middleware
Composes guard functions with a final service handler. Guard return objects are merged into context and the deprecated guard alias.
import { Action, middleware } from '@sourceregistry/sveltekit-service-manager';
const requireAuth = async ({ cookies }) => {
const token = cookies.get('token');
if (!token) throw Action.error(401, { message: 'Unauthorized' } as any);
return { token };
};
export const route = middleware(
async ({ context }) => Action.success(200, { token: context.token }),
requireAuth,
);Only real SvelteKit HTTP errors and redirects are treated as framework control flow. Other thrown values go through middleware error handlers or are rethrown.
Service
Calls a service-local function or returns a local value without HTTP.
import { Service } from '@sourceregistry/sveltekit-service-manager';
const user = await Service('users', 'current');The call is typed through App.Services.
Client API
Import browser/client utilities from @sourceregistry/sveltekit-service-manager/client.
Service(name, config?)
Creates a typed browser caller for public services.
import { Service } from '@sourceregistry/sveltekit-service-manager/client';
const ping = Service('ping');
const result = await ping.call('/health');Route building
ping.route('/health'); // /api/v1/services/ping/healthTo include the current page search params, pass the current URL:
const ping = Service('ping', { url });
ping.route('/health', { includeSearchParams: true });POST JSON
await ping.call('/echo', { message: 'hello' });Passing a body defaults the method to POST, JSON-serializes plain objects, and sets content-type: application/json.
Custom entrypoint or fetch
const ping = Service('ping', {
entryPoint: '/api/v1/services',
executor: fetch,
});Entrypoints with [param] or [...param] placeholders are resolved from config.params.
ServiceError
Failed calls throw ServiceError.
import { ServiceError } from '@sourceregistry/sveltekit-service-manager/client';
try {
await ping.call('/private');
} catch (error) {
if (error instanceof ServiceError) {
console.error(error.code);
console.error(error.data);
}
}Response Helpers
import { Action, error, fail, file, json, text } from '@sourceregistry/sveltekit-service-manager';
Action.success(200, { ok: true });
Action.fail(400, { field: 'email' });
Action.error(401, { message: 'Unauthorized' } as any);
Action.redirect(302, '/login');
json({ ok: true });
text('hello');
file(blob, { mode: 'attachment', filename: 'report.csv' });
fail({ message: 'Bad request' }, { status: 400 });
error({ message: 'Internal error' }, { status: 500 });Security-sensitive behavior:
json()andtext()setContent-Lengthfrom UTF-8 byte length.file()sanitizes the fallbackfilenamevalue and emitsfilename*for encoded names.Action.*responses use JSON bodies with atypeandstatusfield.
Node Adapters
Proxy
Runs a Node-style request listener, such as an Express app, inside a Fetch/SvelteKit service route.
import express from 'express';
import { Proxy } from '@sourceregistry/sveltekit-service-manager';
const app = express();
app.get('/hello', (_req, res) => res.json({ hello: 'world' }));
const proxy = new Proxy(app);
export const service = {
name: 'express-demo',
route: (event) => proxy.handle(event),
};Server
Runs a Router or request handler as a standalone HTTP/HTTPS server.
import { Router, Server } from '@sourceregistry/sveltekit-service-manager';
const router = Router().GET('/health', () => new Response('ok'));
new Server(
{
router,
origin: 'https://api.example.com',
allowedHosts: ['api.example.com'],
},
{ type: 'http' },
).listen(3000);Standalone server hardening:
- malformed
Hostheaders are rejected; allowedHostsrestricts accepted hostnames;originpinsevent.urlto a trusted origin;- cookies default to
httpOnly: true,sameSite: 'lax', andsecure: trueoutside localhost. Callers may override these options explicitly.
Production Guidance
- Use explicit gateway allow-lists with stable
accessKeyvalues. - Add auth in router
prehooks ormiddlewareguards. - Use route methods deliberately and rely on built-in
405andAllowresponses. - Keep service
load()side-effect-light and release resources incleanup(). - Use
Service()for internal in-process calls when HTTP is unnecessary. - For standalone servers, configure
originandallowedHosts. - Add tests for access control, method restrictions, auth failures, and nested routing.
- Keep dependency audit clean in CI.
Type Reference
// Gateway and service management
ServiceManager.Base(selector?, options?)
ServiceManager.Load(service, importMeta?)
ServiceManager.Reload(name)
ServiceManager.Internal(name, ...args)
// Router
Router()
ServiceRouter
RouteHandler<Path>
PreRouteHandler
PostRouteHandler
RequestMethods
// Service contracts
Service<T, Args, Local>
ServiceHandler<Params, RouteId>
ServiceRequestEvent<Params, RouteId>
ServiceEndpoint
// Client
Service(name, config?)
ServiceError
PublicServices
// Node adapters
Proxy
ServerExports
Server
ServiceManagerServiceRouter/RouterServicefor internal callsActionmiddlewareServerProxyjson,text,html,file,fail,error
Client
ServiceServiceErrorPublicServices
Testing
npm test
npm run check