How to buildOctober 11, 202611 min read
Backend DevelopmentStep 4 of 13

NestJS: modules, controllers, services and DTOs

In short

NestJS is a Node.js framework that adds structure on top of Express: code is split into modules, controllers handle HTTP, logic lives in service providers, and a built-in dependency injection container wires them together. Input is described with DTO classes decorated with class-validator rules and checked by ValidationPipe before the handler runs. To test a whole module, Nest boots the application inside a test with @nestjs/testing, and supertest sends the requests.

TutorialLevel: IntermediateNestJSPractice: ~1.5–3 h

After this you will be able to

  • Create a module with a controller and a service using the Nest CLI and explain who creates the service instance and when
  • Describe input with a DTO class and enable ValidationPipe with whitelist and forbidNonWhitelisted
  • Throw NotFoundException and ConflictException from a service and get the correct 404 and 409 responses
  • Write an e2e test for the module with @nestjs/testing and supertest that checks 201, 400, 404 and 409

Verified with

NestJS 12.1 (@nestjs/core 12.1.2, @nestjs/cli 12.0.8)TypeScript 6.0.3class-validator 0.15.1, class-transformer 0.5.1Vitest 4.1, supertest 7.3Express 5.2 (@nestjs/platform-express)Node.js 24 LTS (24.18)

Checked against docs: October 11, 2026

Contents
  1. 01Why NestJS when you already have Express
  2. 02The project: Nest CLI and layout
  3. 03Modules, controllers, providers and dependency injection
  4. 04DTOs and ValidationPipe
  5. 05Exceptions: 404, 409 and a consistent format
  6. 06Tests: a service unit test and a module e2e test
  7. 07Common mistakes and limitations
  8. 08Exercises

Why NestJS when you already have Express

Express gives you routes and middleware (functions that run during request handling) and imposes nothing else. With five handlers in one file that is a strength. With twenty resources and three developers, everyone arranges code differently: validation inside one route, a separate function in another, and the database client imported directly from anywhere. Such code is hard to test because dependencies hide in imports.

NestJS solves an organisational problem. Under the hood it runs the same Express by default (the @nestjs/platform-express package; Fastify is an option), but it adds consistent rules: a module declares what a part of the application consists of; a controller handles HTTP only; a service holds the logic; objects never construct each other but receive dependencies through the constructor from the Nest container.

Route

Express
app.post("/leads", handler)
NestJS
A class method decorated with @Post() inside @Controller("leads")

Request body validation

Express
Custom middleware or a schema call in the handler
NestJS
DTO class + ValidationPipe; the handler receives already-validated data

Handler-to-logic link

Express
Direct module import
NestJS
Constructor-based dependency injection

HTTP errors

Express
Custom error handler (err, req, res, next)
NestJS
Built-in NotFoundException, ConflictException and the exceptions layer

Testing a whole module

Express
supertest against the exported app
NestJS
Test.createTestingModule + supertest, with provider overrides

The price of structure is more files and more concepts: decorators, modules, providers. For a small service with a couple of routes Express is the honest choice. NestJS pays off when the backend has several domains, a team and a long life.

The project: Nest CLI and layout

The Nest CLI creates the project. According to the docs, NestJS 12 runs on Node.js 20.19+ (or 22.12+ on the 22 line), while the CLI generators need Node.js 22.22.3+, 24.15+ or 26+. The flags below make scaffolding non-interactive: npm as the package manager, no git repository and no @nestjs/observe observability module.

bash
npx @nestjs/cli@12 new leads-api --package-manager npm --skip-git --no-observe
cd leads-api
npx nest g module leads
npx nest g controller leads
npx nest g service leads
npm install class-validator class-transformer
A new project plus module, controller and service stubs for leads
terminal
$ npx nest g module leads
CREATE src/leads/leads.module.ts (86 bytes)
UPDATE src/app.module.ts (331 bytes)
$ npx nest g controller leads
CREATE src/leads/leads.controller.ts (103 bytes)
CREATE src/leads/leads.controller.spec.ts (506 bytes)
UPDATE src/leads/leads.module.ts (177 bytes)
$ npx nest g service leads
CREATE src/leads/leads.service.ts (93 bytes)
CREATE src/leads/leads.service.spec.ts (474 bytes)
UPDATE src/leads/leads.module.ts (257 bytes)
Real generator output: each step registers itself in the module

What matters about the NestJS 12 template:

  • The project is ESM by default: package.json contains "type": "module". Relative imports therefore carry a .js extension — import { LeadsService } from './leads.service.js' — even though the file is .ts. This is a Node.js rule for ES modules, and the v12 migration guide calls it out.
  • The test runner for an ESM project is Vitest (Jest for CommonJS). npm test runs unit tests *.spec.ts; npm run test:e2e runs *.e2e-spec.ts from the test folder with a separate config.
  • The linter is oxlint and the formatter is Prettier. npm run build calls nest build and writes JavaScript to dist.
  • tsconfig.json enables experimentalDecorators and emitDecoratorMetadata, which decorators and dependency injection rely on. strictPropertyInitialization is off, so DTO fields without initial values compile under strict: true.

The generated app.controller.ts, app.service.ts and their «Hello World!» test are removed in this lesson — the root module only imports the leads module. The generated leads.controller.spec.ts is removed too: the controller receives the service through its constructor, so the empty test without a provider would fail.

Modules, controllers, providers and dependency injection

Start with the data model. Lead statuses are a constant array and the type is derived from it: one source of truth for TypeScript and for runtime validation.

src/leads/lead.ts
export const LEAD_STATUSES = ['new', 'in_progress', 'done'] as const;
export type LeadStatus = (typeof LEAD_STATUSES)[number];

export interface Lead {
  id: string;
  name: string;
  phone: string;
  message?: string;
  status: LeadStatus;
  createdAt: string;
}
The lead model

A service is a provider: a class decorated with @Injectable(). The NestJS docs describe a provider as something that can be injected as a dependency. Domain rules live in the service: no second open lead for the same phone, no status change for a lead that does not exist.

src/leads/leads.service.ts
import { randomUUID } from 'node:crypto';
import {
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { CreateLeadDto } from './dto/create-lead.dto.js';
import type { Lead, LeadStatus } from './lead.js';

@Injectable()
export class LeadsService {
  private readonly leads = new Map<string, Lead>();

  create(dto: CreateLeadDto): Lead {
    const open = [...this.leads.values()].find(
      (lead) => lead.phone === dto.phone && lead.status !== 'done',
    );
    if (open) {
      throw new ConflictException(
        'An open lead with this phone already exists',
        {
          errorCode: 'LEAD_DUPLICATE',
        },
      );
    }
    const lead: Lead = {
      id: randomUUID(),
      name: dto.name,
      phone: dto.phone,
      message: dto.message,
      status: 'new',
      createdAt: new Date().toISOString(),
    };
    this.leads.set(lead.id, lead);
    return lead;
  }

  findAll(): Lead[] {
    return [...this.leads.values()];
  }

  findOne(id: string): Lead {
    const lead = this.leads.get(id);
    if (!lead) {
      throw new NotFoundException(`Lead ${id} not found`);
    }
    return lead;
  }

  updateStatus(id: string, status: LeadStatus): Lead {
    const lead = this.findOne(id);
    lead.status = status;
    return lead;
  }

  remove(id: string): void {
    this.findOne(id);
    this.leads.delete(id);
  }
}
The service: in-memory storage and rules; errors are built-in Nest exceptions

The controller accepts HTTP and hands the work to the service. The @Controller('leads') prefix is joined with the method path, so @Get(':id') serves GET /leads/:id. The default response status is 200, or 201 for POST; @HttpCode(204) changes it for deletion, where the response has no body.

src/leads/leads.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  ParseUUIDPipe,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateLeadDto } from './dto/create-lead.dto.js';
import { UpdateLeadStatusDto } from './dto/update-lead-status.dto.js';
import type { Lead } from './lead.js';
import { LeadsService } from './leads.service.js';

@Controller('leads')
export class LeadsController {
  constructor(private readonly leadsService: LeadsService) {}

  @Post()
  create(@Body() dto: CreateLeadDto): Lead {
    return this.leadsService.create(dto);
  }

  @Get()
  findAll(): Lead[] {
    return this.leadsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string): Lead {
    return this.leadsService.findOne(id);
  }

  @Patch(':id/status')
  updateStatus(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: UpdateLeadStatusDto,
  ): Lead {
    return this.leadsService.updateStatus(id, dto.status);
  }

  @Delete(':id')
  @HttpCode(204)
  remove(@Param('id', ParseUUIDPipe) id: string): void {
    this.leadsService.remove(id);
  }
}
The controller: routes, parameters and a service call — nothing else

The module ties the pieces together. controllers lists what Nest must instantiate and register as routes; providers lists what the container may inject inside the module. The root AppModule imports the leads module.

src/leads/leads.module.ts
import { Module } from '@nestjs/common';
import { LeadsController } from './leads.controller.js';
import { LeadsService } from './leads.service.js';

@Module({
  controllers: [LeadsController],
  providers: [LeadsService],
})
export class LeadsModule {}
The leads module
src/app.module.ts
import { Module } from '@nestjs/common';
import { LeadsModule } from './leads/leads.module.js';

@Module({
  imports: [LeadsModule],
})
export class AppModule {}
The root module after removing the template AppController and AppService
  1. 01HTTP request
  2. 02ValidationPipe
  3. 03LeadsController
  4. 04LeadsService
  5. 05In-memory Map
The path of POST /leads through the module

How injection works. The constructor constructor(private readonly leadsService: LeadsService) does not create the service; it declares that it needs one. At startup Nest reads the constructor parameter type metadata (TypeScript emits it thanks to emitDecoratorMetadata), finds LeadsService among the module providers, instantiates it and passes it to the controller. Per the docs the default lifetime is singleton: the instance is created once and shared with every consumer. That is why the Map inside the service lives as long as the process.

Modules are encapsulated: a provider is visible only inside its own module. If, say, a notifications module needs the leads service, add the service to the leads module exports and the leads module to the notifications module imports. Otherwise Nest reports at startup that it cannot resolve the dependency.

DTOs and ValidationPipe

A DTO (Data Transfer Object) describes the shape of incoming request data. In NestJS DTOs are classes, not interfaces: TypeScript interfaces are erased at compile time, while Nest pipes need the type at runtime. Validation rules come from class-validator decorators.

src/leads/dto/create-lead.dto.ts
import {
  IsNotEmpty,
  IsOptional,
  IsString,
  Matches,
  MaxLength,
} from 'class-validator';

export class CreateLeadDto {
  @IsString()
  @IsNotEmpty()
  @MaxLength(100)
  name: string;

  @Matches(/^\+7\d{10}$/, {
    message: 'phone must be in the format +7XXXXXXXXXX',
  })
  phone: string;

  @IsOptional()
  @IsString()
  @MaxLength(1000)
  message?: string;
}
POST /leads body: name required, phone as +7XXXXXXXXXX, message optional
src/leads/dto/update-lead-status.dto.ts
import { IsIn } from 'class-validator';
import { LEAD_STATUSES, type LeadStatus } from '../lead.js';

export class UpdateLeadStatusDto {
  @IsIn(LEAD_STATUSES)
  status: LeadStatus;
}
PATCH /leads/:id/status body: an allowed status only

A pipe in Nest runs right before the handler: it receives the arguments, validates or transforms them, and either passes them on or throws — in which case the handler is never called. ValidationPipe turns the request body into a DTO instance and runs the class-validator rules. The ParseUUIDPipe in the controller works the same way for a single path parameter.

src/setup.ts
import { type INestApplication, ValidationPipe } from '@nestjs/common';

export function configureApp(app: INestApplication): void {
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );
}
Global app setup lives in one function that both main.ts and the e2e test call
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { configureApp } from './setup.js';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  configureApp(app);
  await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();
Entry point
  • whitelist: true strips properties that are not in the DTO. Without it a client could send role or status and, if some code spreads the whole body, get them stored.
  • forbidNonWhitelisted: true turns an unknown property into a 400 instead of silently dropping it, so the client learns about a typo in a field name right away.
  • The default error format is errorFormat: 'list' — an array of strings in message. NestJS 12 adds a 'grouped' option: an object keyed by property path.
terminal
$ curl -i -X POST localhost:3000/leads -H 'Content-Type: application/json' \
    -d '{"name":"Aida","phone":"+77011234567"}'
HTTP/1.1 201 Created
{"id":"546c1f10-d53a-4e09-af7a-3c8a8e17cd39","name":"Aida","phone":"+77011234567","status":"new","createdAt":"2026-10-11T23:12:57.267Z"}

$ curl -i -X POST localhost:3000/leads -H 'Content-Type: application/json' \
    -d '{"name":"","phone":"8701","role":"admin"}'
HTTP/1.1 400 Bad Request
{"message":["property role should not exist","name should not be empty","phone must be in the format +7XXXXXXXXXX"],"error":"Bad Request","statusCode":400}
Real responses from the running app: success and three violations in one request

If the project already has Zod schemas — for example from the Express lesson — NestJS 12 offers StandardSchemaValidationPipe: it validates parameters that declare a schema in the decorator (@Body({ schema: createLeadSchema })) and passes everything else through. Both pipes can be enabled together.

Exceptions: 404, 409 and a consistent format

Nest has a built-in exceptions layer: when a handler or service throws an HttpException or a subclass, the client receives the matching status code and a JSON body. Any other exception becomes a 500 with the body {"statusCode": 500, "message": "Internal server error"}, so internal details do not leak.

BadRequestException

Code
400
When to throw
Input is formally invalid and no pipe caught it

UnauthorizedException

Code
401
When to throw
Authentication is missing or wrong

ForbiddenException

Code
403
When to throw
The user is known but lacks permission

NotFoundException

Code
404
When to throw
No resource with this identifier

ConflictException

Code
409
When to throw
The request contradicts current state: a duplicate, a concurrent change

UnprocessableEntityException

Code
422
When to throw
Well-formed data fails a business check — if your team separates 400 from 422

You can pass an errorCode in the second argument — a stable machine-readable code that appears in the response body. The human-facing message may change, while the client (website or mobile app) branches on errorCode.

terminal
$ curl -i -X POST localhost:3000/leads -H 'Content-Type: application/json' \
    -d '{"name":"Aida","phone":"+77011234567"}'
HTTP/1.1 409 Conflict
{"message":"An open lead with this phone already exists","error":"Conflict","statusCode":409,"errorCode":"LEAD_DUPLICATE"}

$ curl -i localhost:3000/leads/42
HTTP/1.1 400 Bad Request
{"message":"Validation failed (uuid is expected)","error":"Bad Request","statusCode":400}

$ curl -i localhost:3000/leads/8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b
HTTP/1.1 404 Not Found
{"message":"Lead 8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b not found","error":"Not Found","statusCode":404}
Real responses: a duplicate, an invalid UUID (rejected by ParseUUIDPipe), a missing lead

The default shape is statusCode, message, error. If your API already returns errors as Problem Details (RFC 9457) with the application/problem+json type — as in the Express lesson — change the shape with an exception filter: a class decorated with @Catch() that implements ExceptionFilter, registered globally with app.useGlobalFilters(). That filter is the last exercise of this lesson.

Tests: a service unit test and a module e2e test

Test.createTestingModule takes the same metadata as @Module(), and compile() asynchronously builds the dependency graph. A unit test of the service needs just one provider.

src/leads/leads.service.spec.ts
import { ConflictException, NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { LeadsService } from './leads.service.js';

describe('LeadsService', () => {
  let service: LeadsService;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [LeadsService],
    }).compile();
    service = moduleRef.get(LeadsService);
  });

  it('creates a lead with status "new"', () => {
    const lead = service.create({ name: 'Aida', phone: '+77011234567' });
    expect(lead.status).toBe('new');
    expect(service.findOne(lead.id)).toEqual(lead);
  });

  it('rejects a second open lead with the same phone', () => {
    service.create({ name: 'Aida', phone: '+77011234567' });
    expect(() =>
      service.create({ name: 'Aida', phone: '+77011234567' }),
    ).toThrow(ConflictException);
  });

  it('allows a new lead once the previous one is done', () => {
    const first = service.create({ name: 'Aida', phone: '+77011234567' });
    service.updateStatus(first.id, 'done');
    const second = service.create({ name: 'Aida', phone: '+77011234567' });
    expect(second.id).not.toBe(first.id);
  });

  it('throws NotFoundException for an unknown id', () => {
    expect(() =>
      service.findOne('8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b'),
    ).toThrow(NotFoundException);
  });
});
A unit test of the service rules without HTTP

An e2e (end-to-end) test boots the whole AppModule: createNestApplication() creates a real application, init() initialises it, and supertest sends HTTP requests to app.getHttpServer() without opening a port. app.close() after each test releases resources; a fresh application in beforeEach also means fresh storage for every test.

test/leads.e2e-spec.ts
import { type INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { type App } from 'supertest/types.js';
import { AppModule } from '../src/app.module.js';
import { configureApp } from '../src/setup.js';

describe('Leads (e2e)', () => {
  let app: INestApplication<App>;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    app = moduleRef.createNestApplication();
    configureApp(app);
    await app.init();
  });

  afterEach(async () => {
    await app.close();
  });

  it('POST /leads creates a lead (201)', async () => {
    const res = await request(app.getHttpServer())
      .post('/leads')
      .send({ name: 'Aida', phone: '+77011234567' })
      .expect(201);
    expect(res.body).toMatchObject({ name: 'Aida', status: 'new' });
    expect(res.body.id).toEqual(expect.any(String));
  });

  it('POST /leads rejects an invalid body (400)', async () => {
    const res = await request(app.getHttpServer())
      .post('/leads')
      .send({ name: '', phone: '8701', role: 'admin' })
      .expect(400);
    expect(res.body.message).toEqual(
      expect.arrayContaining([
        'property role should not exist',
        'name should not be empty',
        'phone must be in the format +7XXXXXXXXXX',
      ]),
    );
  });

  it('POST /leads returns 409 for a duplicate open lead', async () => {
    const lead = { name: 'Aida', phone: '+77011234567' };
    await request(app.getHttpServer()).post('/leads').send(lead).expect(201);
    const res = await request(app.getHttpServer())
      .post('/leads')
      .send(lead)
      .expect(409);
    expect(res.body.errorCode).toBe('LEAD_DUPLICATE');
  });

  it('GET /leads/:id returns 400 for a non-UUID and 404 for an unknown id', async () => {
    await request(app.getHttpServer()).get('/leads/42').expect(400);
    await request(app.getHttpServer())
      .get('/leads/8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b')
      .expect(404);
  });

  it('PATCH status and DELETE (204)', async () => {
    const { body: lead } = await request(app.getHttpServer())
      .post('/leads')
      .send({ name: 'Aida', phone: '+77011234567' })
      .expect(201);
    await request(app.getHttpServer())
      .patch(`/leads/${lead.id}/status`)
      .send({ status: 'archived' })
      .expect(400);
    const { body: updated } = await request(app.getHttpServer())
      .patch(`/leads/${lead.id}/status`)
      .send({ status: 'in_progress' })
      .expect(200);
    expect(updated.status).toBe('in_progress');
    await request(app.getHttpServer()).delete(`/leads/${lead.id}`).expect(204);
    await request(app.getHttpServer()).get(`/leads/${lead.id}`).expect(404);
  });
});
E2e test: 201, 400, 409, 404, status change and deletion
terminal
$ npm test -- --reporter=verbose

 RUN  v4.1.11 …/leads-api

 ✓ src/leads/leads.service.spec.ts > LeadsService > creates a lead with status "new" 408ms
 ✓ src/leads/leads.service.spec.ts > LeadsService > rejects a second open lead with the same phone 6ms
 ✓ src/leads/leads.service.spec.ts > LeadsService > allows a new lead once the previous one is done 5ms
 ✓ src/leads/leads.service.spec.ts > LeadsService > throws NotFoundException for an unknown id 11ms

 Test Files  1 passed (1)
      Tests  4 passed (4)

$ npm run test:e2e -- --reporter=verbose

 RUN  v4.1.11 …/leads-api

 ✓ test/leads.e2e-spec.ts > Leads (e2e) > POST /leads creates a lead (201) 196ms
 ✓ test/leads.e2e-spec.ts > Leads (e2e) > POST /leads rejects an invalid body (400) 17ms
 ✓ test/leads.e2e-spec.ts > Leads (e2e) > POST /leads returns 409 for a duplicate open lead 17ms
 ✓ test/leads.e2e-spec.ts > Leads (e2e) > GET /leads/:id returns 400 for a non-UUID and 404 for an unknown id 16ms
 ✓ test/leads.e2e-spec.ts > Leads (e2e) > PATCH status and DELETE (204) 24ms

 Test Files  1 passed (1)
      Tests  5 passed (5)
Real output: 4 unit tests and 5 e2e tests passed

Why the test calls configureApp(app). Tests do not run main.ts, so whatever is set up there with app.useGlobalPipes is missing from the test application. We checked this by commenting the call out: the application accepts invalid data.

terminal
$ npm run test:e2e

 × POST /leads rejects an invalid body (400) 14ms
 × PATCH status and DELETE (204) 15ms

 FAIL  test/leads.e2e-spec.ts > Leads (e2e) > POST /leads rejects an invalid body (400)
Error: expected 400 "Bad Request", got 201 "Created"
 FAIL  test/leads.e2e-spec.ts > Leads (e2e) > PATCH status and DELETE (204)
Error: expected 400 "Bad Request", got 200 "OK"

      Tests  2 failed | 3 passed (5)
Real output without configureApp: validation is off in the test, 2 tests failed

Hence the rule: global setup lives in one function called by both main.ts and the tests. The alternative is to register the global pipe as a module provider with the APP_PIPE token, so it comes along with the module.

Common mistakes and limitations

At startup: Nest can't resolve dependencies of the LeadsController (?)

Cause
The service is missing from the module providers or lives in another module without exports/imports
Fix
Add the provider to its module, or export it and import the module

Invalid data gets through with a 201

Cause
No ValidationPipe (often only in tests), or the DTO is imported with import type
Fix
One setup function for main.ts and tests; a regular DTO import

At build time: TS2307: Cannot find module './leads.service'

Cause
A relative import without .js in an ESM project (moduleResolution: nodenext)
Fix
Write ./leads.service.js

@Param('id') id: number holds a string

Cause
Path and query parameters always arrive as strings
Fix
ParseIntPipe / ParseUUIDPipe or transform: true on ValidationPipe

Data disappears after a restart

Cause
Storage is a Map inside a singleton service
Fix
A teaching simplification; next comes a database behind a repository
  • Each area is its own module; the root module only imports them
  • The controller holds no rules: route, parameters, service call
  • Body and parameters are validated before the handler: DTO + ValidationPipe, Parse…Pipe for path params
  • Errors are built-in exceptions with the right code, plus a stable errorCode for clients
  • Global pipes and filters are wired identically in main.ts and in the e2e test
  • The e2e test covers 400, 404 and 409, not just the happy path

What this lesson leaves out and a real service will need: database storage behind a repository, authentication and permissions (guards), configuration from environment variables, OpenAPI documentation. Each is a separate lesson in this track; the module → controller → service structure stays the same.

Exercises

Next step: the lesson on backend application layers, where storage moves from the service into a repository while rules and transactions stay in the service. Backend tests against a real database are in the backend testing lesson.

How this was verified7 · passedchecked 11 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.