Skip to main content
0.0.x

NetScript vs backend frameworks

NetScript carries one contract from request to client, then keeps the job typed

Validation is the route contract, the consumer imports that same contract, and the endpoint hands a validated payload to a named worker. There is no transport DTO to reconcile with a client shape later.

Nest.js: Nest’s controller and dependency-injection model is mature; validation DTOs, Swagger client generation, BullMQ registration, and processor classes are separate surfaces you keep aligned.

// contracts/reports.ts
import { baseContract } from '@netscript/contracts';
import { z } from 'zod';

const CreateReport = z.object({
  accountId: z.string().uuid(),
  format: z.enum(['pdf', 'csv']),
});
const Report = z.object({
  id: z.string().uuid(),
  status: z.literal('queued'),
});
export const ReportsContractV1 = {
  create: baseContract
    .route({ method: 'POST', path: '/reports' })
    .input(CreateReport)
    .output(Report),
};

// services/reports/router.ts
import { implement } from '@orpc/server';
import { createServiceClient } from '@netscript/sdk/client';
import { workersContract } from '@netscript/plugin-workers/contracts';

const ReportsV1 = implement(ReportsContractV1);
const workers = createServiceClient<typeof workersContract>({
  contract: workersContract,
  serviceName: 'workers-api',
  routerName: 'workers',
});
export const router = {
  create: ReportsV1.create.handler(async ({ input }) => {
    const report = await reports.insert(input);
    await workers.triggerJob({
      id: 'render-report',
      payload: { reportId: report.id },
    });
    return report;
  }),
};

// consumers/reports-client.ts
export const reportsClient = createServiceClient<typeof ReportsContractV1>({
  contract: ReportsContractV1,
  serviceName: 'reports',
});
await reportsClient.create({ accountId, format: 'pdf' });

// plugins/workers/jobs/render-report.ts
import { createSuccessResult, defineJobHandler } from '@netscript/plugin-workers-core';
import { z } from 'zod';
const RenderReport = z.object({ reportId: z.string().uuid() });
type RenderReport = z.infer<typeof RenderReport>;
const renderReport = defineJobHandler<RenderReport>(async (ctx) => {
  const payload = RenderReport.parse(ctx.payload);
  await renderPdf(payload.reportId);
  return createSuccessResult({ reportId: payload.reportId });
});
export default Object.assign(renderReport, { id: 'render-report' as const });
// create-report.dto.ts
export class CreateReportDto {
  @IsUUID() accountId!: string;
  @IsIn(['pdf', 'csv']) format!: 'pdf' | 'csv';
}

// main.ts
app.useGlobalPipes(new ValidationPipe({ transform: true }));

// reports.controller.ts
@Controller('reports')
export class ReportsController {
  constructor(@InjectQueue('reports') private queue: Queue) {}

  @Post()
  async create(@Body() input: CreateReportDto): Promise<Report> {
    const report = await reports.insert(input);
    await this.queue.add('render-report', { reportId: report.id });
    return report;
  }
}

// reports.processor.ts
@Processor('reports')
export class ReportsProcessor extends WorkerHost {
  async process(job: Job<{ reportId: string }>) {
    if (job.name === 'render-report') await renderPdf(job.data.reportId);
  }
}

// consumer.ts — generated from the Swagger document
import { createReport } from './generated-client.ts';
await createReport({ accountId, format: 'pdf' });

NetScript turns a contract change into a consumer compile error

The consumer imports the contract object; nothing is generated. Add a required field to CreateReport, and every stale call becomes a TypeScript error at build time.

import { ReportsContractV1 } from '@acme/contracts';
import { createServiceClient } from '@netscript/sdk/client';

const reports = createServiceClient({ contract: ReportsContractV1, serviceName: 'reports' });
await reports.create({ accountId, format: 'pdf' });

NetScript keeps more of a backend change in one typed surface

These are architectural estimates for this generic report endpoint, not benchmark results.

Architectural estimate NetScript composition Convention-split equivalent
Request-shape sources to keep aligned 1 contract object ~2–4 DTO, schema, and client surfaces
Consumer client after a contract change Imported directly Regenerated or hand-updated
Files touched to add one required request field ~2 ~3–5
Architectural surface visible at entry ~87–93% ~40–53%