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.
npm install @sourceregistry/sveltekit-service-manager
Peer dependency: svelte ^5.0.0
In this repository examples may import from $lib/server/index.js. In applications, import from @sourceregistry/sveltekit-service-manager.
// 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/hello
Import server utilities from @sourceregistry/sveltekit-service-manager or @sourceregistry/sveltekit-service-manager/server.
ServiceManager.BaseCreates 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.LoadRegisters 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.
RouterCreates 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.
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'.
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.
middlewareComposes 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.
ServiceCalls 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.
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');
ping.route('/health'); // /api/v1/services/ping/health
To include the current page search params, pass the current URL:
const ping = Service('ping', { url });
ping.route('/health', { includeSearchParams: true });
await ping.call('/echo', { message: 'hello' });
Passing a body defaults the method to POST, JSON-serializes plain objects, and sets content-type: application/json.
const ping = Service('ping', {
entryPoint: '/api/v1/services',
executor: fetch,
});
Entrypoints with [param] or [...param] placeholders are resolved from config.params.
ServiceErrorFailed 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);
}
}
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() and text() set Content-Length from UTF-8 byte length.file() sanitizes the fallback filename value and emits filename* for encoded names.Action.* responses use JSON bodies with a type and status field.ProxyRuns 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),
};
ServerRuns 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:
Host headers are rejected;allowedHosts restricts accepted hostnames;origin pins event.url to a trusted origin;httpOnly: true, sameSite: 'lax', and secure: true outside localhost. Callers may override these options explicitly.accessKey values.pre hooks or middleware guards.405 and Allow responses.load() side-effect-light and release resources in cleanup().Service() for internal in-process calls when HTTP is unnecessary.origin and allowedHosts.// 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
Server
ServiceManagerServiceRouter / RouterService for internal callsActionmiddlewareServerProxyjson, text, html, file, fail, errorServiceServiceErrorPublicServicesnpm test
npm run check