tspace-spear
Version:
tspace-spear is a lightweight, high-performance API framework for Node.js that leverages the native HTTP server and supports uWebSockets.js (C++) for maximum speed and efficiency.
1,512 lines (1,243 loc) โข 34.5 kB
Markdown
# tspace-spear
[](https://www.npmjs.com)
[](https://www.npmjs.com)
**tspace-spear** is a lightweight, high-performance API framework for Node.js, built on the native HTTP server with optional support for uWebSockets.js (C++) to achieve maximum speed and efficiency.
It is designed with a strong focus on developer experience and provides end-to-end (E2E) type safety and testing support across the full request lifecycle, from request input to response output (see [E2E](#e2e)).
---
## Features
- โก High-performance core built on native Node.js HTTP
- ๐ Optional [uWebSockets.js](#adapter) adapter support for ultra-low latency and maximum throughput
- ๐ง End-to-end [E2E](#e2e) type safety across the entire request โ response lifecycle
- ๐ฎ Built-in support for [Controllers](#controller) and route-based architecture
- ๐ท๏ธ Powerful Decorator system for routes, middleware, validation, and metadata
- ๐ฆ [DTO](#dto) (Data Transfer Object) support for structured and type-safe request handling
- ๐ Built-in [File Upload](#file-upload) support via `useFileUpload()` with zero configuration required
- ๐ Native [WebSocket](#web-socket) support for real-time applications and event-driven systems
- โ๏ธ [GraphQL](#graphql) support with flexible schema integration and HTTP adapters
- ๐ฅ๏ธ Built-in [cluster mode](#cluster) support for multi-core scalability and higher throughput
- ๐งช Built-in testing utilities for [E2E](#e2e) validation
- ๐งฉ Simple and intuitive developer experience
- ๐ Auto-generated [Swagger](#swagger) documentation via `app.useSwagger()` with zero manual configuration
- ๐ฅ Lightweight and optimized for high-performance APIs and microservices
---
## Install
Install with [npm](https://www.npmjs.com/):
```sh
npm install tspace-spear --save
npm install tspace-spear -g
```
## Documentation
See the [`docs`](https://thanathip41.github.io/tspace-spear) directory for full documentation.
## Basic Usage
- [Getting Started](#getting-started)
- [Quick Started](#quick-started)
- [Adapter](#adapter)
- [Cluster](#cluster)
- [Global Prefix](#global-prefix)
- [Logger](#logger)
- [Format Response](#format-response)
- [Notfound](#notfound)
- [Response](#response)
- [Catch](#catch)
- [Cors](#cors)
- [Body](#body)
- [File Upload](#file-upload)
- [Cookie](#cookie)
- [Middleware](#middleware)
- [Controller](#controller)
- [Dto](#dto)
- [Router](#router)
- [Swagger](#swagger)
- [WebSocket](#websocket)
- [Graphql](#graphql)
- [E2E](#e2e)
## Getting Started
```js
import { Spear } from "tspace-spear";
new Spear()
.get('/' , () => 'Hello world!')
.get('/json' , () => {
return {
message : 'Hello world!'
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
```
## Quick Started
Generate applications, modules, controllers, services, and middleware with the Spear CLI.
```sh
# Install CLI globally
npm install -g tspace-spear
# Create a new application structure
spear create new my-app
โ Successfully created project "my-app"
๐ฆ Project Structure
src/
โโโ common/
โ โโโ middlewares/
โ โโโ log.middleware.ts
โ
โโโ modules/
โ โโโ cats/
โ โโโ cat.controller.ts
โ โโโ cat.service.ts
โ โโโ cat.dto.ts
โ
โโโ client.ts
โโโ index.ts
๐ Next Steps
cd my-app
npm run dev
โ Server is running at:
http://localhost:8000
โ Swagger Docs:
http://localhost:8000/api/docs
โ Run E2E client:
ts-node src/client.ts
ts-node src/client.ts // for E2E
```
## Adapter
tspace-spear supports multiple server adapters,
including the native Node.js HTTP server and uWebSockets.js for high performance.
โ ๏ธ Requirements for uWebSockets.js
Node.js 18 or higher is required
Installation is done via GitHub (no official npm release)
```js
import { Spear } from "tspace-spear";
import uWS from "uWebSockets.js";
// Install via package.json
// "dependencies": {
// "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.45.0"
// }
new Spear({ adapter: uWS })
.get("/", () => "Hello world!")
.get("/json", () => {
return {
message: "Hello world!",
};
})
.listen(8000, () =>
console.log("uWS server is running at http://localhost:8000")
);
```
## Cluster
Cluster mode allows tspace-spear to run multiple worker processes to fully utilize multi-core CPU performance.
```js
import { Spear } from "tspace-spear";
new Spear({
cluster : 3
})
.get('/' , () => 'Hello world!')
.get('/json' , () => {
return {
message : 'Hello world!'
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
```
## Global Prefix
Global Prefix allows you to define a base path for all routes in your application.
It helps keep your API structured and consistent (e.g. /api, /v1, /app).
```js
const app = new Spear({
globalPrefix : '/api' // prefix all routes
})
.get('/' , () => 'Hello world!')
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// http://localhost:8000/api => 'Hello world!'
```
## Logger
The built-in Logger provides request logging for incoming HTTP requests.
It helps you monitor:
Request method
Request path
Performance tracking
Debugging and observability
You can enable a simple logger or configure advanced logging behavior.
```js
const app = new Spear({
logger : true
})
// or use this for logging
.useLogger({
methods : ['GET','POST'],
exceptPath : /\/benchmark(\/|$)|\/favicon\.ico(\/|$)/ // or use Array ['/']
})
.get('/' , () => 'Hello world!')
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
```
## Format Response
Provides a consistent response structure system to standardize how responses are handled across your application.
It ensures that:
* All responses are predictable
* Errors are properly structured
* Missing routes are handled cleanly
* Global error catching is supported
### Notfound
The NotFound handler is triggered when no route matches the incoming request.
```js
const app = new Spear()
.get('/' , () => {
return {
message: 'Hello world'
}
})
.notfound(({ res } : T.Context) => {
return res.notFound('Not found!')
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// http://localhost:8000/notfound => { success: false , message : 'Not found!' , statusCode: 404 }
```
### Response
The response system ensures that all returned values are automatically formatted and sent to the client.
You can return:
* String
* Object (JSON)
* Custom response via ctx.res
```js
import { Spear } from "tspace-spear";
const app = new Spear()
.get('/' , () => {
return {
message: 'Hello world'
}
})
.response((results, statusCode) => {
if (typeof results === 'string') return results
if (Array.isArray(results)) {
return {
success: statusCode < 400,
data: results,
statusCode
}
}
if (typeof results === 'object' && results !== null) {
return {
success: statusCode < 400,
...results,
statusCode
}
}
return {
success: statusCode < 400,
data: results,
statusCode
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// http://localhost:8000 => { success: true , message : 'Hello World' , statusCode: 200 }
```
### Catch
The Catch handler is used to handle unexpected runtime errors globally.
It acts as a safety layer to prevent server crashes and standardize error responses.
```js
import { Spear } from "tspace-spear";
import { z } from "zod";
const app = new Spear()
.get('/' , () => {
throw new Error('Catching failed')
})
.catch((err, { res } : T.Context) => {
if(err instanceof z.ZodError) {
return res
.status(422)
.json({
success : false,
message: "Validation failed",
issues : err?.issues,
statusCode : 422
});
}
return res
.status(500)
.json({
success : false,
message : err?.message,
statusCode : 500
});
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// http://localhost:8000 => { success: false , message : 'Catching failed' , statusCode: 500 }
```
## Cors
CORS (Cross-Origin Resource Sharing) controls which origins are allowed to access your API.
It helps secure your server by restricting or allowing cross-domain requests.
```js
const app = new Spear()
.cors({
origins: [
/^http:\/\/localhost:\d+$/
],
credentials: true
})
//.cors() allow *
.listen(port , () => console.log(`Server is now allow cors localhost:* `))
```
## Body
Body parsing allows your server to read incoming request payloads (JSON) and access them via ctx.body.
It enables handling requests with structured data.
```js
new Spear()
// enable body payload
.useBodyParser()
.post('/' , ({ body }) => {
return {
yourBody : body
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
```
## File Upload
File upload support allows handling multipart/form-data requests and working with uploaded files via ctx.files.
It provides:
* Temporary file handling
* File size limits
* Manual file movement
* Auto cleanup option
```js
import { Spear, type T } from 'tspace-spear';
import path from 'path'
new Spear()
// use this for enable file upload
.useFileUpload({
limit : 1000 * 1000, // limit for file upload 1_000_000 bytes by default Infinity
tempFileDir : 'temp', // folder temporary directory by default tmp
removeTempFile : {
remove : true, // remove temporary files by default false
ms : 1000 * 60 // remove file temporary after 60 seconds
}
})
.post('/' , ({ files } : T.Context) => {
// you can move the file from temporary to other folder
// for example please validate the your input file
const file = files.file[0]
const folder = 'uploads'
await file.write(path.join(path.resolve(),`${folder}/${+new Date()}.${file.extension}`))
// after writed the file you should remove the temporary file
await file.remove()
return {
files
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
```
## Cookie
Cookie support allows you to read and manage HTTP cookies from incoming requests via ctx.cookies.
It is useful for:
* Session handling
* Authentication
* User preferences
* Stateful requests
```js
new Spear()
.useCookiesParser()
.post('/' , ({ cookies }) => {
return {
yourCookies : cookies
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
```
## Middleware
Middleware is a function that runs before the controller handler and is used to:
* Intercept requests
* Modify ctx
* Validate or block execution
* Handle authentication / logging / transformations
```js
import { type T } from "tspace-spear"
// file cat-middleware.ts
export default (ctx : T.Context, next: T.NextFunction) =>{
console.log('cat middleware globals');
return next();
}
import Spear { Router, type T } from "tspace-spear";
import CatMiddleware from './cat-middleware.ts'
(async () => {
const port = Number(process.env.PORT ?? 8000)
const app = new Spear({
middlewares: [ CatMiddleware ]
// if you want to import middlewares with a directory can you follow the example
// middlewares : {
// folder : `${__dirname}/middlewares`,
// name : /middleware\.(ts|js)$/i
// }
})
// or add a middleware
app.use((ctx : T.Context , next : T.NextFunction) => {
console.log('global middlewares')
return next()
})
app.get('/' ((ctx,next) => {
console.log('middleware on the crrent route')
return next()
}), ({ res } : T.Context) => {
return res.json({
message : 'hello world!'
});
})
app.get('/' , ({ res } : T.Context) => {
return res.json({
message : 'hello world!'
});
})
app.listen(port , () => console.log(`Server is now listening http://localhost:8000`))
// localhost:8000
})()
```
## Controller
A Controller is used to group related routes and define request handlers in a structured way.
It helps organize your application into modules (similar to NestJS / Express routers), while keeping a clean and readable API design.
```js
import {
Controller ,
Middleware ,
Get ,
Post,
Patch,
Put,
Delete,
WriteHeader,
Query,
Body,
Params,
Cookies,
Files,
StatusCode,
type T
} from 'tspace-spear';
import CatMiddleware from './cat-middleware.ts'
// file cat-controller.ts
@Controller('/cats')
class CatController {
@Get('/')
@Middleware(CatMiddleware)
@Query('test','id')
@Cookies('name')
public async index({ query , cookies } : {
query : T.Query<{ id : string }>
cookies : T.Cookies<{ name : string}>
}) {
return {
query,
cookies
}
}
@Get('/:id')
@Middleware(CatMiddleware)
@Params('id')
public async show({ params } : T.Context) {
return {
params
}
}
@Post('/')
@Middleware(CatMiddleware)
public async store({ body } : T.Context) {
return {
body
}
}
@Put('/:id')
@Middleware(CatMiddleware)
public async update({ files } : T.Context) {
return {
files
}
}
@Post('/upload')
@Middleware(CatMiddleware)
public async upload({ files } : T.Context) {
return {
files
}
}
@Delete('/:id')
@Middleware(CatMiddleware)
public async destroy({ params } : T.Context) {
return {
params
}
}
}
import { Spear } , { Router, type T } from "tspace-spear";
import CatController from './cat-controller.ts'
(async () => {
const app = new Spear({
controllers: [ CatController ]
// if you want to import controllers with a directory can you follow the example
// controllers : {
// folder : `${__dirname}/controllers`, // nestjs style `${__dirname}/modules/*`
// name : /controller\.(ts|js)$/i,
// *Auto-generate route metadata for type-safe E2E usage,
// *and swagger documentation. By default if use .useSwagger() in app no need to set any description
// preRouteTypes : true
// }
})
app.useBodyParser()
app.useCookiesParser()
app.useFileUpload()
app.get('/' , ( { res } : T.Context) => {
return res.json({
message : 'hello world!'
});
})
app.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// localhost:8000/cats
// localhost:8000/cats/41
})()
```
## Dto
DTO (Data Transfer Object) is used to validate and transform incoming request data before it reaches your controller logic.
```js
import {
Controller ,
Post,
Validate,
ValidateDto,
createDtoDecorator,
type T
} from 'tspace-spear';
import z from "zod";
import {
IsString,
IsInt,
validate
} from "class-validator";
const ValidateDtoCustomBody = (keys: string[]) => {
return createDtoDecorator((ctx) => {
const body = ctx.body ?? {};
const issues: Array<{ path: string; message: string }> = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (body[key] == null) {
issues.push({
path: key,
message: "Missing field",
});
}
}
if (issues.length > 0) {
throw {
message : "Validation failed",
issues
}
}
});
}
const ValidateDtoPromiseBody = (keys: string[]) => {
return createDtoDecorator(async (ctx) => {
await new Promise(resolve => setTimeout(resolve,500));
// check in DB or other async operation
const cats = await catRepository.findMany({ where : { name : ctx.body.name }});
if(!cats.length) {
throw new Error('Validation failed in promise!');
}
}, (ctx, error) => {
// you implement your custom error handling for async validation here
return ctx.res.status(400).json({
message: error.message || "Validation failed",
issues: error.issues || [],
});
});
}
const catSchema = z.object({
name: z.string(),
age: z.number(),
})
class CreateCatDto {
@IsString()
name!: string;
@IsInt()
age!: number;
}
// file cat-controller.ts
@Controller('/cats')
export class CatController {
@Post('/')
// only required validation without type checking
@Validate(["name", "age"], { required: { allowEmptyString: false, allowNull: false } })
public async basic(ctx : T.Context<{ body : { name : any , age : any }}>) {
return {
body : ctx.body
}
}
@Post('/custom')
@ValidateDtoCustomBody(["name", "age"])
public async custom(ctx : T.Context<{ body : { name : string , age : number }}>) {
return {
body : ctx.body
}
}
@Post('/promise')
@ValidateDtoPromiseBody(['name'])
public async promise(ctx : T.Context<{ body : { name : string }}>) {
return {
body : ctx.body
}
}
@Post('/zod')
@ValidateDto(catSchema, { adaptor : "zod" })
public async zod(ctx : T.Context<{ body : z.infer<typeof catSchema>}>) {
return {
body : ctx.body
}
}
@Post('/cls')
@ValidateDto(CreateCatDto)
public async cls(ctx : T.Context<{ body : CreateCatDto }>) {
return {
body : ctx.body
}
}
}
import { Spear } , { Router, type T } from "tspace-spear";
import CatController from './cat-controller.ts'
(async () => {
new Spear({
controllers: [ CatController ]
})
.useBodyParser()
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// localhost:8000/cats // basic implete
// localhost:8000/cats/zod // zod implete
// localhost:8000/cats/promise // promise implete
})()
```
## Router
The Router allows you to organize routes into modular groups, making your application more scalable and maintainable.
It supports:
* Grouped routes
* Nested route prefixes
* Reusable router modules
* Separation of concerns
```js
import { Spear, Router, type T } from "tspace-spear";
const app = new Spear()
const router = new Router()
router.groups('/my',(r) => {
r.get('/cats' , ({ req , res }) => {
return res.json({
message : 'Hello, World!'
})
})
return r
})
router.get('/cats' , ({ req , res }) => {
return res.json({
message : 'Hello, World!'
})
})
app.useRouter(router)
app.get('/' , ({ res } : T.Context) => {
return res.json({
message : 'hello world!'
});
})
let port = 8000
app.listen(port , () => console.log(`Server is now listening http://localhost:8000`))
// localhost:8000/my/cats
// localhost:8000/cats
```
## Swagger
Provides built-in Swagger support to document your API endpoints.
It allows you to:
* Describe request parameters (query, body, params)
* Generate API documentation
* Improve developer experience
* Standardize API contracts
```js
// file cat-controller.ts
import {
type T,
Controller,
Get ,
Post,
Put,
Patch,
Delete,
Swagger
} from 'tspace-spear';
@Controller('/cats')
class CatController {
@Get('/')
@Swagger({
query : {
id : {
type : 'integer'
},
name : {
type : 'string'
}
}
})
public async index({ query } : T.Context) {
return {
query
}
}
@Get('/:id')
@Swagger({
description : '- message',
query : {
id : {
type : 'integer'
}
},
responses : [
{ status : 200 , description : "OK" , example : { id : 'catz' }},
{ status : 400 , description : "Bad request" , example : { id : 'catz' }}
]
})
public async show({ params } : T.Context) {
return {
params
}
}
@Post('/')
@Swagger({
bearerToken : true,
body : {
description : 'The description !',
required : true,
properties : {
id : {
type : 'integer',
example : 1
},
name : {
type : 'string',
example : "xxxxx"
},
status : {
type : 'string',
example: "active",
enum: ['active', 'inactive'],
description: "User status (active = enabled, inactive = disabled)",
required : true
},
roles: {
type: 'array',
items : {
type: 'string',
example: "roleA",
enum: ['roleA', 'roleB']
}
},
}
}
})
public async store({ body } : T.Context) {
return {
body
}
}
@Put('/:uuid')
@Swagger({
bearerToken : true,
body : {
description : 'The description !',
required : true,
properties : {
id : {
type : 'integer',
example : 1
},
name : {
type : 'string',
example : "xxxxx"
}
}
}
})
public async updated({ body } : T.Context) {
return {
body
}
}
@Patch('/:uuid')
@Swagger({
bearerToken : true,
body : {
description : 'The description !',
required : true,
properties : {
id : {
type : 'integer',
example : 1
},
name : {
type : 'string',
example : "xxxxx"
}
}
}
})
public async update({ body } : T.Context) {
return {
body
}
}
@Delete('/:uuid')
@Swagger({
bearerToken : true
})
public async delete({ params } : T.Context) {
return {
params
}
}
@Post('/upload')
@Swagger({
bearerToken : true,
files : {
required : true,
properties : {
file : {
type : 'array',
items: {
type :"string",
format:"binary"
}
},
name : {
type : 'string'
}
}
}
})
public async upload({ body , files } : T.Context) {
return {
body,
files
}
}
}
(async () => {
await new Spear({
controllers: [ CatController ]
})
.get('/' , ({ res } : T.Context) => {
return res.json({
message : 'hello world!'
});
})
// .useSwagger() // by default path is "/api/docs"
.useSwagger({
path : "/docs",
servers : [
{ url : "http://localhost:8000" , description : "development"},
{ url : "http://localhost:8000" , description : "production"}
],
info : {
"title" : "Welcome to the the documentation",
"description" : "This is the documentation"
}
})
.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`))
// localhost:8000/docs
})()
```
## WebSocket
Provides built-in WebSocket support for real-time communication.
It allows you to:
* Handle client connections
* Send/receive messages
* Build chat systems
* Manage real-time events
```js
import { Spear } from "tspace-spear";
import fs from 'fs';
import path from 'path';
new Spear()
.get('/',(ctx) => {
// you can serve a HTML file for testing WebSocket connection
const htmlWs = fs.readFileSync(path.join(path.resolve(), 'public', 'ws.html'), 'utf-8');
return ctx.res.html(htmlWs);
})
.ws(() => {
const clients = new Map<string, any>();
return {
connection: (ws) => {
console.log("connected");
},
message: (ws, msg) => {
const data = JSON.parse(msg.toString());
if (data.type === "register") {
ws.userId = data.userId;
clients.set(data.userId, ws);
ws.send(JSON.stringify({
type: "system",
message: `registered as ${data.userId}`
}));
return;
}
if (data.type === "chat") {
const targetWs = clients.get(data.to);
if (!targetWs) {
ws.send(JSON.stringify({
type: "error",
message: "User not online"
}));
return;
}
targetWs.send(JSON.stringify({
type: "chat",
from: ws.userId,
text: data.text
}));
return;
}
},
close: (ws) => {
if (ws.userId) {
clients.delete(ws.userId);
}
},
error: (ws, error) => {
console.error('WebSocket error:', error);
}
};
})
.listen(8000 , ({ port , server }) => {
console.log(`server listening on : http://localhost:${port}`)
})
```
## Graphql
GraphQL CRUD Example with graphql-http + tspace-spear
This example shows how to build a simple GraphQL CRUD API using graphql-http and tspace-spear.
It includes:
- GraphQL schema setup
- Query and Mutation examples
- Create / Read / Update / Delete operations
- HTTP integration with graphql-http
- cURL testing examples
The server uses an in-memory array as a fake database for simplicity.
Features
- High performance HTTP server with tspace-spear
- Native GraphQL schema definitions
- Full CRUD operations
- Simple and dependency-light setup
- Works with standard GraphQL clients and tools
```sh
npm install graphql graphql-http
```
```js
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLNonNull,
GraphQLID,
} from 'graphql';
import { createHandler } from 'graphql-http/lib/use/http';
import { type T, Spear } from "tspace-spear";
/**
* Fake database
*/
const users : {
id : string;
name : string;
email :string
}[] = [];
/**
* User Type
*/
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLID },
name: { type: GraphQLString },
email: { type: GraphQLString },
},
});
/**
* Queries (READ)
*/
const QueryType = new GraphQLObjectType({
name: 'Query',
fields: {
users: {
type: new GraphQLList(UserType),
resolve: () => {
return users;
},
},
user: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
},
resolve: (_, args) => {
return users.find((v) => v.id === args.id);
},
},
},
});
/**
* Mutations (CREATE UPDATE DELETE)
*/
const MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
/**
* CREATE
*/
createUser: {
type: UserType,
args: {
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
},
resolve: (_, args) => {
const user = {
id: String(users.length + 1),
name: args.name,
email: args.email,
};
users.push(user);
return user;
},
},
/**
* UPDATE
*/
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
email: { type: GraphQLString },
},
resolve: (_, args) => {
const user = users.find((v) => v.id === args.id);
if (!user) {
throw new Error('User not found');
}
if (args.name !== undefined) {
user.name = args.name;
}
if (args.email !== undefined) {
user.email = args.email;
}
return user;
},
},
/**
* DELETE
*/
deleteUser: {
type: GraphQLString,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
},
resolve: (_, args) => {
const index = users.findIndex((v) => v.id === args.id);
if (index === -1) {
throw new Error('User not found');
}
users.splice(index, 1);
return 'Deleted';
},
},
},
});
/**
* Schema
*/
const schema = new GraphQLSchema({
query: QueryType,
mutation: MutationType,
});
/**
* Handler
*/
const graphqlHandler = createHandler({
schema,
});
const app = new Spear()
.post('/graphql',({ req , res }) => graphqlHandler(req , res))
app.listen(4000 , ({ port , server }) => {
console.log(`server listening on : http://localhost:${port}/graphql`)
})
```
```sh
## Create
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { createUser(name:\"John\", email:\"john@example.com\") { id name email } }"
}'
## Read all
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { users { id name email } }"
}'
## Read one
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { user(id:\"1\") { id name email } }"
}'
## Update
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { updateUser(id:\"1\", name:\"Johnny\") { id name email } }"
}'
## Delete
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { deleteUser(id:\"1\") }"
}'
```
## E2E
Provides end-to-end type safety and testing support across the full request lifecycle, from request input to
response output. It allows you to:
```js
// file cat-controller.ts
import z from 'zod';
import {
type T,
Controller,
Get,
Post,
Put,
Delete,
createDtoDecorator
} from "tspace-spear";
const catSchema = z.object({
id: z.number(),
name: z.string(),
age: z.number(),
});
const catSchemaAction = z.object({
name: z.string(),
age: z.number(),
});
type Cat = z.infer<typeof catSchema>
let cats: z.infer<typeof catSchema>[] = [
{ id: 1, name: 'cat1', age: 1.6 },
{ id: 2, name: 'cat2', age: 1.8 },
];
const ValidateDtoBody = (schema: z.ZodTypeAny) => {
return createDtoDecorator((ctx) => {
const result = schema.parse(ctx.body);
ctx.body = result as T.Body;
});
};
@Controller('/cats')
class CatController {
@Get('/')
public async index({
query,
}: T.Context) {
return {
cats,
};
}
@Get('/:id')
public async show({ res, params }: T.Context<{ params: { id: number } }>) : Promise<{
cat : Cat
}> {
const cat = cats.find((d) => d.id === Number(params.id));
if(cat == null) {
return res.notFound('not found cat')
}
return {
cat
};
}
@Post('/')
@ValidateDtoBody(catSchemaAction)
public async create({
body,
}: T.Context<{ body: z.infer<typeof catSchemaAction> }>) {
const cat = {
id: cats.length + 1,
...body
}
cats.push(cat);
return {
cat,
message: 'created',
};
}
@Put('/:id')
@ValidateDtoBody(catSchemaAction.partial())
public async update({
res,
params,
body,
}: T.Context<{
params: { id: number };
body: Partial<z.infer<typeof catSchemaAction>>;
}>) {
const id = Number(params.id);
const index = cats.findIndex((d) => d.id === id);
if (index === -1) {
return res.notFound('not found cat')
}
cats[index] = {
...cats[index],
...body,
id
};
const cat = cats[index]
return {
message: 'updated',
cat,
};
}
@Delete('/:id')
public async remove({ res, params }: T.Context<{ params: { id: number } }>) {
const id = Number(params.id);
const index = cats.findIndex((d) => d.id === id);
if (index === -1) {
throw res.notFound('not found cat')
}
cats = cats.filter((d) => d.id !== id);
return {
message: 'deleted',
};
}
}
export { CatController };
export default CatController;
// file server/app.ts
import Spear from "tspace-spear";
const app = new Spear({
logger : true,
controllers: {
folder : `${__dirname}/controllers`,
name:/controller\.(ts|js)$/i,
// don't forget to set this option for auto-generate route metadata for type-safe E2E usage,
// and swagger documentation. By default if use .useSwagger() in app no need to set any description
preRouteTypes: true
}
})
app.useGlobalPrefix('api');
app.useBodyParser();
app.listen(8000 , () => console.log(`Server is now listening http://localhost:8000`));
type AppRouter = typeof app.contract;
export { AppRouter }
export default app;
// file frontend/index.ts
import { AppRouter } from "./server/app";
import { ApiClient } from "tspace-spear/client";
const client: ApiClient<AppRouter> = new ApiClient(
`http://localhost:8000/api`
);
const test = await client.get("/catsq"); // Type error: Argument of type '"/catsq"' is not assignable to parameter of type '"/cats" | "/cats/:id" | ... 3 more
const res = await client.get("/cats");
res.data.cats = 1 // Type error: Type 'number' is not assignable to type '{ id: number; name: string; age: number; }[]'
res.data.cats[0].name = 1 // Type error: Type 'number' is not assignable to type 'string'
res.data.cats[0].age = "1.6" // Type error: Type 'string' is not assignable to type 'number'
console.log(res)
// res.ok -> boolean
// res.status -> number
// res.data -> { cats: [{ id: 1, name: 'cat1', age: 1.6 },{ id: 2, name: 'cat2', age: 1.8 }] }
```