APIμμ λ°μνλ μλ¬λ₯Ό μΌκ΄λκ² μ²λ¦¬νκ³ λͺ
νν μλ΅μ μ 곡νλ λ°©λ²μ μμλ΄
λλ€.
μλ¬ μ²λ¦¬ κ°μ
μΌκ΄λ μλ΅
νμ€νλ μλ¬ νμ ν΄λΌμ΄μΈνΈ μΉνμ
μν μ½λ
HTTP μν μ½λ RESTful νμ€
λͺ
νν λ©μμ§
κ°λ°μ μΉνμ μ¬μ©μ μΉνμ
λ‘κΉ
μλ¬ μΆμ λλ²κΉ
μ§μ
SoException
Sonamuλ SoException κΈ°λ°μ μλ¬ ν΄λμ€λ₯Ό μ 곡ν©λλ€. SoExceptionμ throwνλ©΄ νλ μμν¬κ° μλμΌλ‘ μ μ ν HTTP μν μ½λμ ꡬ쑰νλ μλ¬ μλ΅μ μ μ‘ν©λλ€.
κΈ°λ³Έ μ¬μ©λ²
import { NotFoundException } from "sonamu";
class UserModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async get(id: number): Promise<User> {
const rdb = this.getPuri("r");
const user = await rdb.table("users").where("id", id).first();
if (!user) {
throw new NotFoundException("μ¬μ©μλ₯Ό μ°Ύμ μ μμ΅λλ€");
}
return user;
}
}
μ 곡λλ μμΈ ν΄λμ€
Sonamuκ° κΈ°λ³ΈμΌλ‘ μ 곡νλ μμΈ ν΄λμ€μ
λλ€. λͺ¨λ SoExceptionμ μμν©λλ€.
| ν΄λμ€ | μν μ½λ | μ¬μ© μκΈ° |
|---|
BadRequestException | 400 | μλͺ»λ λ§€κ°λ³μ λ± μμ²μ λ¬Έμ κ° μλ κ²½μ° |
UnauthorizedException | 401 | λ‘κ·ΈμΈμ΄ νμνκ±°λ μ κ·Ό κΆνμ΄ μλ κ²½μ° |
NotFoundException | 404 | μ‘΄μ¬νμ§ μλ λ μ½λμ μ κ·Όνλ κ²½μ° |
InternalServerErrorException | 500 | λ΄λΆ μ²λ¦¬ λ‘μ§ λλ μΈλΆ API νΈμΆ μ€λ₯ |
ServiceUnavailableException | 503 | νμ¬ μνμμ μ²λ¦¬κ° λΆκ°λ₯ν κ²½μ° |
TargetNotFoundException | 520 | μ²λ¦¬ λμμ΄ μ‘΄μ¬νμ§ μλ κ²½μ° |
AlreadyProcessedException | 541 | μ΄λ―Έ μ²λ¦¬λ μμ²μΈ κ²½μ° |
DuplicateRowException | 542 | μ€λ³΅μ νμ©νμ§ μλ μΌμ΄μ€μ μ€λ³΅ μμ² |
SoException μμ±μ
λͺ¨λ μμΈ ν΄λμ€λ λμΌν μμ±μ μκ·Έλμ²λ₯Ό κ°μ§λλ€.
constructor(message: LocalizedString, payload?: unknown)
message: μλ¬ λ©μμ§. λ€κ΅μ΄ λ¬Έμμ΄(LocalizedString)μ μ§μν©λλ€.
payload: μΆκ° μ 보. Zod κ²μ¦ μ΄μ λ°°μ΄ λ±μ μ λ¬ν μ μμ΅λλ€.
μ¬μ© μμ
λ€μν μλ¬ μν©
import {
BadRequestException,
UnauthorizedException,
NotFoundException,
DuplicateRowException,
} from "sonamu";
class UserModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async get(id: number): Promise<User> {
const rdb = this.getPuri("r");
const user = await rdb.table("users").where("id", id).first();
if (!user) {
throw new NotFoundException("μ¬μ©μλ₯Ό μ°Ύμ μ μμ΅λλ€");
}
return user;
}
@api({ httpMethod: "POST" })
async create(params: CreateUserParams): Promise<{ userId: number }> {
const context = Sonamu.getContext();
// μΈμ¦ νμΈ
if (!context.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
// μ
λ ₯ κ²μ¦
if (!params.email) {
throw new BadRequestException("μ΄λ©μΌμ νμμ
λλ€");
}
const rdb = this.getPuri("r");
// μ€λ³΅ νμΈ
const existing = await rdb.table("users").where("email", params.email).first();
if (existing) {
throw new DuplicateRowException("μ΄λ―Έ μ¬μ© μ€μΈ μ΄λ©μΌμ
λλ€");
}
const wdb = this.getPuri("w");
const [user] = await wdb.table("users").insert(params).returning({ id: "id" });
return { userId: user.id };
}
}
payloadλ₯Ό νμ©ν μμΈ μ 보 μ λ¬
import { BadRequestException } from "sonamu";
class OrderModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: CreateOrderParams): Promise<{ orderId: number }> {
// payloadλ‘ μλ¬ μμΈ μ 보 μ λ¬
if (params.quantity <= 0) {
throw new BadRequestException("μ£Όλ¬Έ μλμ΄ μ¬λ°λ₯΄μ§ μμ΅λλ€", {
field: "quantity",
value: params.quantity,
constraint: "must be > 0",
});
}
// ...
}
}
isSoException νμ
κ°λ
import { isSoException, AlreadyProcessedException } from "sonamu";
class PaymentModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async process(paymentId: number): Promise<void> {
try {
await this.executePayment(paymentId);
} catch (error) {
if (isSoException(error) && error.statusCode === 541) {
// μ΄λ―Έ μ²λ¦¬λ κ²°μ - 무μ
return;
}
throw error;
}
}
}
Zod κ²μ¦ μλ¬ μ²λ¦¬
BadRequestExceptionμ payloadμ Zod μ΄μ λ°°μ΄μ μ λ¬νλ©΄, Sonamuμ μλ¬ νΈλ€λ¬κ° μλμΌλ‘ κ²μ¦ μλ¬ μμΈ μ 보λ₯Ό μλ΅μ ν¬ν¨ν©λλ€.
Zod μλ¬ λ³ν
import { z } from "zod";
import { BadRequestException } from "sonamu";
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
password: z.string().min(8),
});
class UserModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: unknown): Promise<{ userId: number }> {
const result = CreateUserSchema.safeParse(params);
if (!result.success) {
throw new BadRequestException("κ²μ¦ μ€ν¨", result.error.issues);
}
const validated = result.data;
const wdb = this.getPuri("w");
const [user] = await wdb.table("users").insert(validated).returning({ id: "id" });
return { userId: user.id };
}
}
μλ¬ μλ΅ νμ
Sonamuμ λ΄μ₯ μλ¬ νΈλ€λ¬λ λ€μ νμμΌλ‘ μλ΅ν©λλ€.
κΈ°λ³Έ μλ¬ μλ΅
{
"name": "NotFoundException",
"code": null,
"message": "μ¬μ©μλ₯Ό μ°Ύμ μ μμ΅λλ€"
}
Zod κ²μ¦ μλ¬ μλ΅ (payloadμ μ΄μ λ°°μ΄ μ λ¬ μ)
{
"name": "BadRequestException",
"code": null,
"message": "κ²μ¦ μ€ν¨ (email)",
"issues": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["email"],
"message": "Required"
}
]
}
μλ¬ νΈλ€λ¬ 컀μ€ν°λ§μ΄μ§
Sonamuλ κΈ°λ³Έ μλ¬ νΈλ€λ¬λ₯Ό λ΄μ₯νκ³ μμ§λ§, νμ μ startServerμ lifecycle.onError μ΅μ
μΌλ‘ 컀μ€ν
μλ¬ νΈλ€λ¬λ₯Ό μ€μ ν μ μμ΅λλ€.
import { Sonamu, isSoException } from "sonamu";
Sonamu.startServer({
lifecycle: {
onError: (error, request, reply) => {
// 컀μ€ν
μλ¬ μ²λ¦¬ λ‘μ§
if (isSoException(error)) {
reply.status(error.statusCode).send({
error: error.message,
payload: error.payload,
});
} else {
reply.status(500).send({
error: "Internal server error",
});
}
},
},
});
μν μ½λ μ°Έμ‘°ν
νμ€ HTTP μν μ½λ
| μ½λ | μ΄λ¦ | μ¬μ© μκΈ° |
|---|
| 200 | OK | μ±κ³΅ (GET, PUT) |
| 201 | Created | 리μμ€ μμ± μ±κ³΅ (POST) |
| 204 | No Content | μ±κ³΅, μλ΅ μμ (DELETE) |
| 400 | Bad Request | μλͺ»λ μμ² νμ |
| 401 | Unauthorized | μΈμ¦ νμ |
| 404 | Not Found | 리μμ€ μμ |
| 500 | Internal Server Error | μλ² λ΄λΆ μλ¬ |
| 503 | Service Unavailable | μλΉμ€ μ΄μ© λΆκ° |
Sonamu 컀μ€ν
μν μ½λ
| μ½λ | μμΈ ν΄λμ€ | μ¬μ© μκΈ° |
|---|
| 520 | TargetNotFoundException | μ²λ¦¬ λμμ΄ μ‘΄μ¬νμ§ μμ |
| 541 | AlreadyProcessedException | μ΄λ―Έ μ²λ¦¬λ μμ² |
| 542 | DuplicateRowException | μ€λ³΅ νμ©νμ§ μλ κ³³μ μ€λ³΅ |
μ€μ ν¨ν΄
import {
UnauthorizedException,
BadRequestException,
DuplicateRowException,
} from "sonamu";
class UserModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: unknown): Promise<{ userId: number }> {
const context = Sonamu.getContext();
// 1. μΈμ¦ νμΈ
if (!context.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
// 2. Zod κ²μ¦
const validated = CreateUserSchema.safeParse(params);
if (!validated.success) {
throw new BadRequestException("κ²μ¦ μ€ν¨", validated.error.issues);
}
const data = validated.data;
// 3. λΉμ¦λμ€ κ·μΉ κ²μ¦
const rdb = this.getPuri("r");
const existing = await rdb.table("users").where("email", data.email).first();
if (existing) {
throw new DuplicateRowException("μ΄λ―Έ μ¬μ© μ€μΈ μ΄λ©μΌμ
λλ€");
}
// 4. μμ±
const wdb = this.getPuri("w");
const [user] = await wdb.table("users").insert(data).returning({ id: "id" });
return { userId: user.id };
}
}
μ£Όμμ¬ν
μλ¬ μ²λ¦¬ μ μ£Όμμ¬ν: 1. λ―Όκ°ν μ 보 λ
ΈμΆ κΈμ§ (μ€ν νΈλ μ΄μ€, DB μλ¬ λ±) 2. μ μ ν HTTP
μν μ½λ μ¬μ© 3. λͺ
ννκ³ μΌκ΄λ μλ¬ λ©μμ§ 4. μλ¬λ νμ λ‘κΉ
5. νλ‘λμ
μμλ μμΈ μ 보 μ ν
λ€μ λ¨κ³
μλ κ²μ¦
Zod κΈ°λ° κ²μ¦
컀μ€ν
κ²μ¦
컀μ€ν
κ²μ¦ λ‘μ§
@api λ°μ½λ μ΄ν°
API κΈ°λ³Έ μ¬μ©λ²