Merhaba arkadaşlar nodejs için yapmaya çalıştığım bir Rest api kütüphanesini tanıtmak istiyorum. Test suitini bildiğimde buraya npm linkini eklerim
Neptune Nedir?
Basitce Neptune object-driven rest api frameworku dur.
Peki neden Neptune
Neptune giriş kaynağım express ve express kullanırken karşılaştığım bazı sorunlar. Bu sorunlar üzerinden geliştirdiğim Microp. Neptune için ise Microp'un evriminin son hali diyebilirim.
Neptuneun kullanımı
Neptune app
Bos bir neptune uygulaması oluşturmak bu kadar kolay
import { createNeptune } from "@.../neptune/app";
export const app = createNeptune({
adapter: NeptuneNodeAdapter,
hostname: "localhost",
port: 3000,
resources: [],
}).run();
Resource
Her neptune resource uygulamamızın bir route'unu temsil ediyor
Bir users resource umuz var diyelim. /users e düsen her istek bu resource da karşılanıyor.
Resource basitçe bir class NeptunResource abstract class ini extend ediyor.
// userResource.ts
import { NeptunResource } from "@.../common";
export class UsersResource extends NeptuneResource {
// bu bir dizinde olabilir ["/users" , "/user"]
// bu durumda hem /users e hemde /user e atılan istek burada karşılanır
public path = "/users";
GET() {
const users = ["jdoe", "mdoe", "bdoe"]; // dummy users
return Response.json(users, 200, { "Content-Type": "application/json" });
}
}
import { NeptuneNodeAdapter } from "@.../neptune/adapter";
import { createNeptune } from "@.../neptune/app";
import { UserResource } from "./path/to/userResource";
export const app = createNeptune({
adapter: NeptuneNodeAdapter,
hostname: "localhost",
port: 3000,
resources: [UserResource],
}).run();
Gordugunuz gibi expressin her isi yapan bir routeri yerine bir işi çok iyi yapan bir resource var . Ve her resource bütün http metotlarını handle edebilir ( get, post, put, patch, delete, head, options, connect, trace). Bu sayede daha da modüler bir yapıya sahip.
Body parsing
Neptune body parse edebilmek icin 3.parti paketlere ihtiyac durmaz. Her turlu boundaryi parse edebilir
//upload.ts
import { NeptunResource } from "@.../common";
import type { NeptuneFormData, NeptuneFile } from "@.../internal";
export class UploadResource extends NeptuneResource {
public path = "/user/upload";
// body parse etmek asenkron bir islem oldugundan async await kullanacam
async POST(request: NeptuneRequest) {
const data: NeptuneFormData = await request.formData();
const profilePhoto: NeptuneFile = data.get("profile-photo");
//content type'i text/... haricinde olan hersey icin bize aslinda Readable stream olarak donuyor
try {
await createWriteStream("kaydedilecek/konum/resim.png").pipe(
profilePhoto
);
return Response.null(200);
} catch {
return Response.null(400);
}
}
}
NeptuneRequest objesinin body parsing için sahip olduğu metotlar Text(), Json(), Blob(), FormData(), Buffer(), Xml(), Query()
Sending Response Body
Neptune kendi içinde her turlu datayı serialize edip yollayabilir.
import { NeptunResource } from "@.../common";
import { createReadStream } from "fs";
class DownloadResource extends NeptuneResource {
public path = ["/download", "icon.ico"];
async GET(request: NeptuneRequest) {
if (request.path !== "icon.ico") {
// Dosya stream olarak gonderildi.
return createReadStream("gonderilecek/dosya.png").pipe(
Response.head({
"Content-Type": "img/png",
"Content-Disposition": 'attachment; inline; filename="filename.jpg"',
})
);
}
// NeptuneResponse .File() methodunada sahip ancak bu büyük dosyalarda memory sorunlarına neden olabilir
return await Response.File("icon.ico", 200, {
"Content-Type": "img/png",
"Content-Disposition": 'attachment; inline; filename="filename.jpg"',
});
}
}
Request parametresi almak
neptune /foo/bar , /foo/:bar , /\/foo\/[a-z]+/, /foo/[a-z] seklindeki parametrelerin tümünü destekler
export class UserResource extends NeptuneResource {
public path = "/user/:id";
GET() {
const users = ["jdoe", "mdoe", "bdoe"]; // dummy users
// this.param("id") => "1"
// this.params() => {id: "1"}
return Response.Text(users[this.param("id") as number]);
}
}
/user/1 e Get istegi atildigini varsayalim
aldigimiz cevap "jdoe" olacaktir
Error handling
Neptune error handling icin cesitli imkanlar sunar. Bunlardan biri NeptuneError yardımcı sınıfı
//userNotFoundError.ts
import { NeptuneError } from from "@.../common";
export class UserNotFoundError extends NeptuneError {
message = "User not found";
status = 404
}
import { NeptuneResource } from from "@.../common";
import type { INeptuneError } from from "@.../common";
import { UserNotFoundError } from "../path/to/userNotFoundError"
export class UserResource extends NeptuneResource {
public path = "/user/:id";
GET() {
const users = ["jdoe", "mdoe", "bdoe"]; // dummy users
if(!users[this.param("id")]) throw new UserNotFoundError()
return Response.Text(users[this.param("id") as number]);
}
/*
Resource içinde her hangi bir hata fırlatılırsa bu endpointe düşer
Burada gonderilecek responsa dair herseyi override edebilirsiniz
Bu eger NeptuneError dan turetilmis bir class firlatildi ise bu endpoint zorunlu degildir ve tanimlanmadigi taktirde otomatik olarak fırlatılan Error sinifi dönecektir
{
status: 404,
body: {
message: "User not found"
}
}
*/
ERROR(error: INeptuneError ) {
if(error instanceof UserNotFoundError) {
return this.setHeader("x-user", "not found")
}
else return Response.null(500)
}
}
/user/1 e istek atildigini varsayalim
gelecek response su sekilde olacak
{
"status": 200,
"body": "mdoe",
"headers": {...}
}
eger /user/4 e istek atmis olsaydik
{
"status": 404,
"body" : { "message" : "User not found"},
"headers": {
...,
"x-user": "not found"
}
}
Gelecek veri tipini zorunlu tutma
Bunun için NeptuneInput yardımcı sınıfından türetilmiş bir interface yada sınıf kullanmak yeterli yeterli
import { NeptuneInput } from from "@.../common";
// sadece typescript
export interface ILoginInput extends NeptuneInput {
password: string;
username: string;
}
// yada
export class LoginInput extends NeptuneInput {
password: string;
username: string;
}
Generic kullanmak sadece Typescript
// imports...
export class LoginResource extends NeptuneResource {
public path = "/login";
async POST(request) {
const { password, username } = await requset.json<IUserInput>()
// eger istenilen input gelirse sorunsuz bir sekilde ilerleyecek
// gelmezse ise InputIsNotValid { error : InputNotValidError, message: "<T> InputNotValid"} hatasi firlatilacak, ERROR handlerinda karsilayabilirsiniz
return Response.Text("Wellcome user");
}
}
Declarative yontem
import { NeptuneResource } from from "@.../common";
import { ILoginInput } from "../path/to/loginInput";
export class LoginResource extends NeptuneResource {
public path = "/login";
// POST methoduna gelen body inputa uygun olmazsa ayni process buradada isleyecek
input = {
POST: ILoginInput
}
async POST(request) {
const { password, username } = await requset.json()
return Response.Text("Wellcome user");
}
}
Servisler
Servisleri Express middleware leri gibi dusune bilirsiniz. Ancak dahada fazlasi var
Servislerde basitce bir class NeptuneService abstract classindan turetiliyor
//authService.ts
import { NeptuneService } from from "@.../common";
class AuthService extends NeptuneService {
runBeforeResource(request) {
const token = this.cookies("token")
if(!token) throw new IsNotAuthError() // dummy authError classi
const verify = jwt.verify(token) // dummy jwt :D
if(!token) throw new TokenIsNotValidError() // dummy tokenNotValidError classi
// Keyleri string tipinde olan her hangi bir obje donebilirsiniz buna daha sonra gelecez
return {
session: verify
}
}
// eger ERROR methodu service icinde tanimlandiysa mutlaka response donmelidir.
// tanimlanmadi ise Resource daki error methodu calisacaktir. eger o da yoksada error classina gore response donecektir
ERROR (error) {
Response.null(401)
}
}
Soyle bir seneryomuz olsun, kullanici addressini degistirmek istiyor diyelim. Bu resourca gelmeden once giris yapip yapmadigini kontrol ediyoruz. Eger giris yapti ise buradaki pathc methodu calissin yoksa 401 gonderip istegi sonlandirsin
//...imports
export class AddressResource extends NeptuneResource {
public path = "/user/address";
// bu resourca HER PATHC istegi geldiginde pathc dizinindeki her servis sirasi ile calisacak
services: {
PATHC: [AuthService] // service ler bir biri ardina chain edilebilir
}
async PATCH(request) {
const address = await requset.<IAddressInput>json()
// servislerin dondugu her objeye this.locals den ulasabiliriz. servislerin dondugu her locals objesi bir birine merge edilir. ayrica locals objesi servislerde de ulasilabilirdir. bir servisin dondugu locals objesine diger servisten ulasilabilir
const user = this.locals.session; // Service den tanidik geldi mi?
// providerlera servislerden sonra gelecez
const newAddress = await (this.providers.getProvider("UserProvider")).getUser(user).setAddress(address)
return Response.json(newAddress)
}
}
Providers
Provider lari viewModel gibi dusunebilirsiniz.
Providerlar NeptuneProvider abstract classindan turetiliyor. Providerlar Transient, singleton ve scoped olarak calisabilirler ve Neptune de di containerlarinin temelini olusturur
// userProvider.ts
import { NeptuneProvider } from "@.../ioc";
export class UserProvider extends NeptuneProvider {
scope = "transient"; // "transient" , "singleton" , "scoped"
name = "UserProvider"; // providerlara ulasmak icin gerekli
findUserById(id) {
// fake UserModel
return UserModel.findById(id);
}
}
//...imports
export const app = createNeptune({
adapter: NeptuneNodeAdapter,
hostname: "localhost",
port: 3000,
resources: [UserResource],
providers: [UserProvider], // providerimizi ekledik
}).run();
providerlara herhangi bir resource, service veya baska bir providerdan erisilebilir
//...imports
export class UserResource extends NeptuneResource {
public path = "/user/:id";
async GET(request) {
// Providerlara ulasmak asenkrondur, sadece cagirildiklarinda inject edilir ve sonrasinda dispose edilir
const userProvider = await this.providers.getProvider("UserProvider");
return Response.json<Partial<User>>(
userProvider.findUserById(this.param("id"))
);
}
}