# 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.

Compare NetScript with  Nest.js Hono Encore.dev

**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.

**Hono:** Hono is the smallest credible version of this endpoint and its RPC client is excellent; the background job belongs to the hosting adapter, so portability stops at that boundary.

**Encore.dev:** Encore makes typed endpoints and provisioned Pub/Sub impressively compact; NetScript keeps the contract object, service client, and worker runtime explicit package surfaces you can compose outside one compiler.

```ts
// 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 });
```

```ts
// 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' });
```

```ts
import { Hono } from 'hono';
import { hc } from 'hono/client';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

type RenderReport = { reportId: string };
type Bindings = { REPORTS: Queue<RenderReport> };
const CreateReport = z.object({
  accountId: z.string().uuid(),
  format: z.enum(['pdf', 'csv']),
});

const app = new Hono<{ Bindings: Bindings }>()
  .post('/reports', zValidator('json', CreateReport), async (c) => {
    const report = await reports.insert(c.req.valid('json'));
    await c.env.REPORTS.send({ reportId: report.id });
    return c.json(report, 202);
  });

export type AppType = typeof app;
export const client = hc<AppType>('/');
await client.reports.$post({ json: { accountId, format: 'pdf' } });

export default {
  fetch: app.fetch,
  async queue(batch: MessageBatch<RenderReport>) {
    for (const message of batch.messages) {
      await renderPdf(message.body.reportId);
      message.ack();
    }
  },
};
```

```ts
import { api } from 'encore.dev/api';
import { Subscription, Topic } from 'encore.dev/pubsub';

interface CreateReport {
  accountId: string;
  format: 'pdf' | 'csv';
}
interface Report { id: string; status: 'queued' }
interface RenderReport { reportId: string }

const renders = new Topic<RenderReport>('render-reports', {
  deliveryGuarantee: 'at-least-once',
});

export const create = api(
  { expose: true, method: 'POST', path: '/reports' },
  async (input: CreateReport): Promise<Report> => {
    const report = await reports.insert(input);
    await renders.publish({ reportId: report.id });
    return report;
  },
);

const _render = new Subscription(renders, 'render-report', {
  handler: async ({ reportId }) => { await renderPdf(reportId); },
});

// A consumer in another Encore service
import { reports } from '~encore/clients';
await reports.create({ 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.

```ts
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% |

[Frontend frameworks](https://rickylabs.github.io/netscript/netscript/comparisons/frontend/) [Comparisons](https://rickylabs.github.io/netscript/netscript/comparisons/)
