Middleware functions intercept every request before it reaches a handler. They are the right place for authentication, logging, rate limiting, header injection, and request transformation.
import{corsMiddleware}from'./http/auth.middleware.js';app.use(corsMiddleware(['https://app.example.com']));// Specific originsapp.use(corsMiddleware(['*']));// Any origin (dev only)
Handles preflight OPTIONS requests automatically. Returns 204 for preflight.
Recursively sanitizes all string values in ctx.body before the handler sees them. Strips HTML tags, javascript: protocol, onerror= attributes, and null bytes.
JWT authentication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import{authMiddleware}from'./http/auth.middleware.js';import{JwtService}from'./security/jwt.js';constjwt=newJwtService(config.jwtSecret);constauth=authMiddleware(jwt);// Global (all routes require auth)app.use(auth);// Controller-level (all routes in this controller)@Controller('/api/admin',auth)// Route-level (only this route)@Delete('/:id',auth)
On success, sets ctx.user = { id, email, roles }.
On failure, throws UnauthorizedException.
exportconstresponseTimer:MiddlewareFn=async (ctx,next)=>{conststart=Date.now();awaitnext();// Handler runs hereconstelapsed=Date.now()-start;// Runs after handlerctx.setHeader('X-Response-Time',`${elapsed}ms`);};
Validation
The @Validate decorator attaches schema validation to any route. Validation runs as middleware — before the handler, after authentication.
ValidationSchema structure
1
2
3
4
5
6
7
8
9
10
11
12
13
interfaceValidationSchema{body?:Record<string,FieldRule>;query?:Record<string,FieldRule>;params?:Record<string,FieldRule>;}interfaceFieldRule{type:'string'|'number'|'boolean'|'email'|'uuid';required?:boolean;// default: falsemin?:number;// min string length or numeric valuemax?:number;// max string length or numeric valuepattern?:RegExp;// must match this regex}
Validation examples
Validating a request body
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constregisterSchema:ValidationSchema={body:{email:{type:'email',required:true,max:320},name:{type:'string',required:true,min:1,max:100},password:{type:'string',required:true,min:8,max:128},age:{type:'number',required:false,min:18,max:120},},};@Post('/')@Validate(registerSchema)asyncregister(ctx:StreetContext):Promise<void>{// Validation passed — body is safe to castconstdto=ctx.bodyas CreateUserDto;ctx.json(awaitthis.service.register(dto),201);}
Validating route params
1
2
3
4
5
6
7
8
9
10
11
12
constbyIdSchema:ValidationSchema={params:{id:{type:'uuid',required:true},},};@Get('/:id')@Validate(byIdSchema)asyncgetOne(ctx:StreetContext):Promise<void>{constid=ctx.params['id']!;// Guaranteed to be a valid UUID stringctx.json(awaitthis.service.findById(id));}
When validation fails, the handler does not run. The response is:
1
2
3
4
5
6
7
8
9
10
{"error":"BadRequestException","message":"Validation failed","status":400,"details":["body.email is required","body.password must be at least 8 chars","body.age must be a number"]}
All validation errors are collected before returning — you get all failures at once, not just the first.
Exception Handling
Throw a StreetException subclass from any handler or middleware. The global error handler catches it and formats the JSON response automatically.
// Simple messagethrownewNotFoundException('User not found');// With structured detailsthrownewConflictException('Email already registered',{field:'email',value:'alice@example.com',});// Access the JSON shapeconstex=newBadRequestException('Bad input',['field.name is required']);ex.toJSON();// { error: 'BadRequestException', message: 'Bad input', status: 400, details: [...] }
Handling database errors
Wrap database operations and convert errors:
1
2
3
4
5
6
7
8
9
10
11
12
@Post('/')asynccreate(ctx:StreetContext):Promise<void>{try{constproduct=awaitthis.products.create(ctx.bodyas CreateProductDto);ctx.json(product,201);}catch (err){if (errinstanceofError&&err.message.includes('unique')){thrownewConflictException('Product with this SKU already exists');}throwerr;// Re-throw unknown errors — global handler catches them}}
OpenAPI
street generates an OpenAPI 3.1 spec from your registered routes automatically. No separate spec file to maintain.
Accessing the spec
1
curl http://localhost:3000/api/openapi.json | jq
The HealthController exposes this endpoint at /api/openapi.json. The spec is generated once at startup and cached in ctx.state.
Adding operation metadata
1
2
3
4
5
6
7
8
9
10
11
12
13
import{ApiOperation}from'../core/decorators.js';@Get('/:id')@ApiOperation({summary:'Get user by ID',description:'Returns a single user object. Returns 404 if the user does not exist.',tags:['users'],responses:{'200':{description:'User found',schema:{$ref:'#/components/schemas/User'}},'404':{description:'User not found',schema:{$ref:'#/components/schemas/Error'}},},})asyncgetOne(ctx:StreetContext):Promise<void>{/* ... */}
Example generated spec (excerpt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{"openapi":"3.1.0","info":{"title":"StreetJS API","version":"1.0.0"},"paths":{"/api/users/{id}":{"get":{"summary":"Get user by ID","tags":["users"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User found"},"404":{"description":"User not found"}}}}}}
Path parameters (:id style) are automatically converted to {id} style in the spec.