rest-service
REST Service (implementation) package for @furystack/rest
Usage
You can start with importing your custom API Endpoint interface (see @furystack/rest
) and with the .useRestService<MyApi>(...)
injector extensions method. You can define multiple REST services per injector (even on the same port)
Implementing a custom API
Usage example - Authenticated GET, GET Collection and POST APIs for a custom entity that has a physical store and repository set up
import { MyApi, MyEntity } from 'my-common-package'
import {
createGetCollectionEndpoint,
createGetEntityEndpoint,
Authenticate,
createPostEndpoint,
} from '@furystack/rest-service'
myInjector.useHttpAuthentication().useRestService<MyApi>({
port: 8080,
root: '/api',
cors: {
credentials: true,
origins: ['https://my-frontend-1', 'https://my-frontend-2'],
},
api: {
GET: {
'/my-entities': Authenticate()(createGetCollectionEndpoint({ model: MyEntity, primaryKey: 'id' })),
'/my-entities/:id': Authenticate()(createGetEntityEndpoint({ model: MyEntity, primaryKey: 'id' })),
},
POST: {
'/my-entities': Authenticate()(createPostEndpoint({ model: MyEntity, primaryKey: 'id' })),
},
},
})
Endpoint generators (based on Repository DataSets)
If you use the underlying layers of FuryStack (PhysicalStore
-> Repository
) for an entity type, you can easily create some CRUD endpoints for them. These are the followings:
- createDeleteEndpoint()
- createGetCollectionEndpoint()
- createGetEntityEndpoint()
- createPatchEndpoint()
- createPostEndpoint()
The endpoints will use the defined Physical Stores for retrieving entities and the Repository for authorization / event subscriptions.
Custom endpoint implementation
If you want to implement an endpoint with custom logic, you can define it in the following way:
import { Injector } from '@furystack/inject'
import { RestApi } from '@furystack/rest'
export type MyCustomRequestAction = {
body: {
foo: string
bar: number
}
url: {
entityId: string
}
query: { foo?: string; bar?: number; baz?: boolean }
headers: { foo: string; bar: number; baz: boolean }
result: {
success: boolean
}
}
export interface MyApiWithCustomEndpoint extends RestApi {
POST: {
'/my-custom-request-action/:entityId': MyCustomRequestAction
}
}
import { JsonResult } from '@furystack/rest-service'
const i = new Injector()
i.useRestService<MyApiWithCustomEndpoint>({
port: 8080,
root: '/mockApi',
api: {
POST: {
'/my-custom-request-action/:entityId': async ({
getBody,
getQuery,
getUrlParams,
headers,
injector,
}) => {
const body = await getBody()
console.log(body)
const queryString = getQuery()
console.log(queryString)
const { entityId } = getUrlParams()
console.log('entity id is:', entityId)
console.log('The headers are:', headers)
const currentUser = await injector.getCurrentUser()
console.log('The current user is:', currentUser)
return JsonResult({ success: true }, 200)
},
},
},
})
import { createClient } from '@furystack/rest-client-fetch'
const callApi = createClient<MyApiWithCustomEndpoint>({
endpointUrl: 'https://localhost:8080/mockApi',
})
const getResult = async () =>
callApi({
method: 'POST',
action: '/my-custom-request-action/:entityId',
body: {
foo: 'asd',
bar: 42,
},
headers: {
foo: 'asd',
bar: 2,
baz: false,
},
query: {
foo: 'asd',
},
url: {
entityId: 'asd-123',
},
})
getResult().then((data) => {
console.log(data.result)
console.log(data.response.status)
})
Payload validation
Type-safe APIs does NOT comes with built-in validation by default - but you can use the JSON Schema for full payload validation.
The prefferred way is:
- Create your API interface
- Create JSON Schemas from the API (The
ts-json-schema-generator
package is the best solution nowdays, you can check how it works, here) - Use the Validate middleware, as shown in the following example:
import schema from './path-to-my/generated-schema.json'
const myValidatedApi = Validate({
schema,
schemaName: 'MyCustomRequestAction'
})(...myApiImplementation...)
In that way, you will get full validation for all defined endpoint data (header, body, url parameters, query string) with verbose error messages from ajv
(see integration tests)
Authentication and HttpUserContext
You can use the build-in authentication that comes with this package. It contains a session (~cookie) based authentication and Basic Auth. You can use it with the .useCommonAuth()
injector extension:
myInjector.useCommonAuth({{
cookieName: 'sessionId',
enableBasicAuth: true,
model: ApplicationUserModel,
hashMethod: (plainText) => myHashMethod(plainText),
getSessionStore: (storeManager) => storeManager.getStoreFor(MySessionModel, 'id'),
getUserStore: (storeManager) => storeManager.getStoreFor(ApplicationUserModel, 'id')
}).useRestService<MyApi>({...api options})
Built-in actions
The package contains the following built-in actions
ErrorAction
- for default error handling and dumping errors in the responseGetCurrentUser
- Returns the current userIsAuthenticated
- Returns if a user is logged inLogin
- Login with a simple username + password comboLogout
- Destroys the current sessionNotFoundAction
- The default '404' fallback route