First working version of all examples including swagger.
This commit is contained in:
parent
430a7ed162
commit
8d23b67592
12 changed files with 2101 additions and 0 deletions
55
example2b_swagger.yml
Normal file
55
example2b_swagger.yml
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Swagger UserApi - OpenAPI 3.0
|
||||||
|
description: |-
|
||||||
|
This is a sample User API
|
||||||
|
termsOfService: http://swagger.io/terms/
|
||||||
|
contact:
|
||||||
|
email: apiteam@swagger.io
|
||||||
|
license:
|
||||||
|
name: Apache 2.0
|
||||||
|
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
version: 1.0.11
|
||||||
|
externalDocs:
|
||||||
|
description: Find out more about Swagger
|
||||||
|
url: http://swagger.io
|
||||||
|
tags:
|
||||||
|
- name: user
|
||||||
|
description: Information about user
|
||||||
|
paths:
|
||||||
|
/api/users:
|
||||||
|
post:
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
email:
|
||||||
|
format: email
|
||||||
|
type: string
|
||||||
|
age:
|
||||||
|
minimum: 0
|
||||||
|
type: number
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- email
|
||||||
|
- age
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User created successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
'400':
|
||||||
|
description: Invalid input
|
||||||
1394
package-lock.json
generated
Normal file
1394
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "nodejs-warsaw-xxi",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@git.techserio.com:mattiashz/nodejs-warsaw-xxi.git"
|
||||||
|
},
|
||||||
|
"author": "hz",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/swagger": "^9.4.2",
|
||||||
|
"@fastify/swagger-ui": "^5.2.2",
|
||||||
|
"@fastify/type-provider-typebox": "^5.1.0",
|
||||||
|
"@sinclair/typebox": "^0.34.28",
|
||||||
|
"fastify": "^5.2.1",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/node": "^22.13.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/example1.ts
Normal file
57
src/example1.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post<{ Body: User; }> ('/api/users', async (request, reply) => {
|
||||||
|
const user = request.body;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Try making a POST request with this "deranged" JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "42",
|
||||||
|
"name": 12345,
|
||||||
|
"email": {"address": "bad@example.com"},
|
||||||
|
"age": "twenty-one"
|
||||||
|
}
|
||||||
|
|
||||||
|
What happens:
|
||||||
|
1. TypeScript is happy at compile time (we said body is User)
|
||||||
|
2. Fastify accepts this JSON as valid (no runtime validation)
|
||||||
|
3. Code fails when trying operations like toUpperCase() on a number
|
||||||
|
or comparing "twenty-one" >= 21
|
||||||
|
*/
|
||||||
46
src/example2.ts
Normal file
46
src/example2.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox';
|
||||||
|
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TBUser = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String(),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/users', {
|
||||||
|
schema: {
|
||||||
|
body: TBUser
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const user = request.body as User;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
21
src/example2b.ts
Normal file
21
src/example2b.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Type } from '@sinclair/typebox';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
|
||||||
|
|
||||||
|
// Define the TypeBox schema
|
||||||
|
const TBUser = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String({ format: 'email' }),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//console.log('JSON Schema:');
|
||||||
|
//console.log(JSON.stringify(TBUser, null, 2));
|
||||||
|
|
||||||
|
// Convert to YAML
|
||||||
|
const openApiYaml = yaml.dump(TBUser);
|
||||||
|
|
||||||
|
console.log('JSON Schema (YAML):');
|
||||||
|
console.log(openApiYaml);
|
||||||
46
src/example3.ts
Normal file
46
src/example3.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox';
|
||||||
|
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
||||||
|
|
||||||
|
// Define TypeBox schema as the single source of truth
|
||||||
|
const UserSchema = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String(),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive TypeScript type from the TypeBox schema
|
||||||
|
type User = Static<typeof UserSchema>;
|
||||||
|
|
||||||
|
app.post<{
|
||||||
|
Body: User; // Using the derived TypeScript type
|
||||||
|
}>('/api/users', {
|
||||||
|
schema: {
|
||||||
|
body: UserSchema // Using the TypeBox schema for validation
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
// No type assertion needed - Fastify + TypeBox understands the connection
|
||||||
|
const user = request.body;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
89
src/example4.ts
Normal file
89
src/example4.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox';
|
||||||
|
|
||||||
|
// Create Fastify instance
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
// Set up TypeBox validator
|
||||||
|
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
||||||
|
|
||||||
|
// Define TypeBox schema as the single source of truth
|
||||||
|
const UserSchema = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String(),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive TypeScript type from the TypeBox schema
|
||||||
|
type User = Static<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Response schema for better documentation
|
||||||
|
const ResponseSchema = Type.Object({
|
||||||
|
user: UserSchema,
|
||||||
|
canDrink: Type.Boolean(),
|
||||||
|
greeting: Type.String()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main function to set up the server
|
||||||
|
async function startServer() {
|
||||||
|
// Register Swagger plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger'), {
|
||||||
|
swagger: {
|
||||||
|
info: {
|
||||||
|
title: 'User API',
|
||||||
|
description: 'API for user operations with TypeBox validation',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{ name: 'users', description: 'User related endpoints' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Swagger UI plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger-ui'), {
|
||||||
|
routePrefix: '/swagger',
|
||||||
|
uiConfig: {
|
||||||
|
docExpansion: 'list',
|
||||||
|
deepLinking: false
|
||||||
|
},
|
||||||
|
staticCSP: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register our route
|
||||||
|
app.post('/api/users', {
|
||||||
|
schema: {
|
||||||
|
description: 'Create a new user',
|
||||||
|
tags: ['users'],
|
||||||
|
body: UserSchema,
|
||||||
|
response: {
|
||||||
|
200: ResponseSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const user = request.body as User;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until all plugins are ready
|
||||||
|
await app.ready();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
console.log('Swagger UI available at: http://localhost:3000/swagger');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
startServer();
|
||||||
117
src/example4b.ts
Normal file
117
src/example4b.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox';
|
||||||
|
|
||||||
|
// Create Fastify instance
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
// Set up TypeBox validator
|
||||||
|
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
||||||
|
|
||||||
|
// Define TypeBox schema as the single source of truth
|
||||||
|
const UserSchema = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String(),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derive TypeScript type from the TypeBox schema
|
||||||
|
type User = Static<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Response schema for better documentation
|
||||||
|
const ResponseSchema = Type.Object({
|
||||||
|
user: UserSchema,
|
||||||
|
canDrink: Type.Boolean(),
|
||||||
|
greeting: Type.String()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main function to set up the server
|
||||||
|
async function startServer() {
|
||||||
|
// Register Swagger plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger'), {
|
||||||
|
swagger: {
|
||||||
|
info: {
|
||||||
|
title: 'User API',
|
||||||
|
description: 'API for user operations with TypeBox validation',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{ name: 'users', description: 'User related endpoints' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Swagger UI plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger-ui'), {
|
||||||
|
routePrefix: '/swagger',
|
||||||
|
uiConfig: {
|
||||||
|
docExpansion: 'list',
|
||||||
|
deepLinking: false
|
||||||
|
},
|
||||||
|
staticCSP: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register our route
|
||||||
|
app.post('/api/users', {
|
||||||
|
schema: {
|
||||||
|
description: 'Create a new user',
|
||||||
|
tags: ['users'],
|
||||||
|
body: UserSchema,
|
||||||
|
response: {
|
||||||
|
200: ResponseSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const user = request.body as User;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/users', {
|
||||||
|
schema: {
|
||||||
|
description: 'Get all users',
|
||||||
|
tags: ['users'],
|
||||||
|
response: {
|
||||||
|
200: Type.Array(UserSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
// Sample data that follows our User type
|
||||||
|
const users: User[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
age: 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Jane Smith",
|
||||||
|
email: "jane@example.com",
|
||||||
|
age: 25
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until all plugins are ready
|
||||||
|
await app.ready();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
console.log('Swagger UI available at: http://localhost:3000/swagger');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
startServer();
|
||||||
100
src/example4c.ts
Normal file
100
src/example4c.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox';
|
||||||
|
|
||||||
|
// Create Fastify instance
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
// Set up TypeBox validator
|
||||||
|
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
||||||
|
|
||||||
|
// Define TypeBox schema as the single source of truth
|
||||||
|
const UserSchema = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String(),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
}, { $id: 'User' });
|
||||||
|
|
||||||
|
// Derive TypeScript type from the TypeBox schema
|
||||||
|
type User = Static<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Response schema for better documentation
|
||||||
|
const ResponseSchema = Type.Object({
|
||||||
|
user: Type.Ref(UserSchema),
|
||||||
|
canDrink: Type.Boolean(),
|
||||||
|
greeting: Type.String()
|
||||||
|
}, { $id: 'UserResponse' });
|
||||||
|
|
||||||
|
// Main function to set up the server
|
||||||
|
async function startServer() {
|
||||||
|
// Register schemas with Fastify
|
||||||
|
app.addSchema(UserSchema);
|
||||||
|
app.addSchema(ResponseSchema);
|
||||||
|
|
||||||
|
// Register Swagger plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger'), {
|
||||||
|
swagger: {
|
||||||
|
info: {
|
||||||
|
title: 'User API',
|
||||||
|
description: 'API for user operations with TypeBox validation',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
consumes: ['application/json'],
|
||||||
|
produces: ['application/json'],
|
||||||
|
tags: [
|
||||||
|
{ name: 'users', description: 'User related endpoints' }
|
||||||
|
],
|
||||||
|
// Extract schemas to definitions section
|
||||||
|
definitions: {
|
||||||
|
User: UserSchema as any,
|
||||||
|
UserResponse: ResponseSchema as any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Swagger UI plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger-ui'), {
|
||||||
|
routePrefix: '/swagger',
|
||||||
|
uiConfig: {
|
||||||
|
docExpansion: 'list',
|
||||||
|
deepLinking: false
|
||||||
|
},
|
||||||
|
staticCSP: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register our route
|
||||||
|
app.post('/api/users', {
|
||||||
|
schema: {
|
||||||
|
description: 'Create a new user',
|
||||||
|
tags: ['users'],
|
||||||
|
body: { $ref: 'User#' },
|
||||||
|
response: {
|
||||||
|
200: { $ref: 'UserResponse#' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const user = request.body as User;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until all plugins are ready
|
||||||
|
await app.ready();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
console.log('Swagger UI available at: http://localhost:3000/swagger');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
startServer();
|
||||||
131
src/example4d.ts
Normal file
131
src/example4d.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import fastify from 'fastify';
|
||||||
|
import { Type, Static } from '@sinclair/typebox';
|
||||||
|
import { TypeBoxValidatorCompiler } from '@fastify/type-provider-typebox';
|
||||||
|
|
||||||
|
// Create Fastify instance
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
|
||||||
|
// Set up TypeBox validator
|
||||||
|
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
||||||
|
|
||||||
|
// Define TypeBox schema as the single source of truth
|
||||||
|
const UserSchema = Type.Object({
|
||||||
|
id: Type.Number(),
|
||||||
|
name: Type.String(),
|
||||||
|
email: Type.String(),
|
||||||
|
age: Type.Number({ minimum: 0 })
|
||||||
|
}, { $id: 'User' });
|
||||||
|
|
||||||
|
// Derive TypeScript type from the TypeBox schema
|
||||||
|
type User = Static<typeof UserSchema>;
|
||||||
|
|
||||||
|
// Response schema for better documentation
|
||||||
|
const ResponseSchema = Type.Object({
|
||||||
|
user: UserSchema,
|
||||||
|
canDrink: Type.Boolean(),
|
||||||
|
greeting: Type.String()
|
||||||
|
}, { $id: 'UserResponse' });
|
||||||
|
|
||||||
|
// Main function to set up the server
|
||||||
|
async function startServer() {
|
||||||
|
// Register schemas with Fastify
|
||||||
|
app.addSchema(UserSchema);
|
||||||
|
app.addSchema(ResponseSchema);
|
||||||
|
|
||||||
|
// Register Swagger plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger'), {
|
||||||
|
openapi: {
|
||||||
|
info: {
|
||||||
|
title: 'User API',
|
||||||
|
description: 'API for user operations with TypeBox validation',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
tags: [
|
||||||
|
{ name: 'users', description: 'User related endpoints' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
refResolver: {
|
||||||
|
buildLocalReference: (json, baseUri, fragment, i) => {
|
||||||
|
// Use $id property from schema
|
||||||
|
if (json.$id) {
|
||||||
|
return `#/components/schemas/${json.$id}`;
|
||||||
|
}
|
||||||
|
// Fall back to default behavior
|
||||||
|
return `#/components/schemas/${fragment[1]}${i}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Swagger UI plugin using dynamic import
|
||||||
|
await app.register(import('@fastify/swagger-ui'), {
|
||||||
|
routePrefix: '/swagger',
|
||||||
|
uiConfig: {
|
||||||
|
docExpansion: 'list',
|
||||||
|
deepLinking: false
|
||||||
|
},
|
||||||
|
staticCSP: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register our route
|
||||||
|
app.post('/api/users', {
|
||||||
|
schema: {
|
||||||
|
description: 'Create a new user',
|
||||||
|
tags: ['users'],
|
||||||
|
body: UserSchema,
|
||||||
|
response: {
|
||||||
|
200: ResponseSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
const user = request.body as User;
|
||||||
|
|
||||||
|
const canDrink = user.age >= 21;
|
||||||
|
const greeting = `Hello ${user.name.toUpperCase()}, your email ${user.email.toLowerCase()} has been recorded`;
|
||||||
|
|
||||||
|
return { user, canDrink, greeting };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/users', {
|
||||||
|
schema: {
|
||||||
|
description: 'Get all users',
|
||||||
|
tags: ['users'],
|
||||||
|
response: {
|
||||||
|
200: Type.Array(UserSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, async (request, reply) => {
|
||||||
|
// Sample data that follows our User type
|
||||||
|
const users: User[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "John Doe",
|
||||||
|
email: "john@example.com",
|
||||||
|
age: 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Jane Smith",
|
||||||
|
email: "jane@example.com",
|
||||||
|
age: 25
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return users;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until all plugins are ready
|
||||||
|
await app.ready();
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
try {
|
||||||
|
await app.listen({ port: 3000 });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
console.log('Swagger UI available at: http://localhost:3000/swagger');
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
startServer();
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020.Promise",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"module": "commonjs",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"outDir": "build",
|
||||||
|
"rootDir": "src",
|
||||||
|
"sourceMap": true,
|
||||||
|
"target": "es2017"
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue