Skip to main content

Command Palette

Search for a command to run...

[ 살펴보기 ] Hono - Middlware

Updated
6 min readView as Markdown
[ 살펴보기 ] Hono - Middlware
C

A developer living in Busan, Korea

Middleware를 통해 route handler에 request가 전달되기 전에 request obejct에 접근하거나 response가 client로 전달되기 전에 response object에 접근할 수 있다.

Hono에서 middleware는 app.use를 통해 추가할 수 있다.

import { serve } from "@hono/node-server";
import { Hono } from "hono";

const app = new Hono();

app.use(async (c, next) => {
  console.log(" ::: request before route handler ::: ");
  console.log(c.req.header());
  await next();
  console.log(" ::: response after route handler ::: ");
  console.log(c.res.headers);
});

app.get("/order", (c) => {
  return c.json({ message: "Hello word" });
});

const port = 3000;
console.log(`Server is running on port ${port}`);

serve({
  fetch: app.fetch,
  port,
});

위의 예제에서 볼 수 있듯이 await next() 이전 line에서 request가 route handler에 도달하기 이전 request object에 접근할 수 있고 await next() 이후 line에서 request가 route handler에 의해 처리된 이후에 response object에 접근할 수 있다.

Middleware의 실행 순서를 middleware가 선언된 순서에 따라 결정된다.

app.use(async (c, next) => {
  console.log(" ::: middlware start 1 ::: ");
  await next();
  console.log(" ::: middlware end 1 ::: ");
});

app.use(async (c, next) => {
  console.log(" ::: middlware start 2 ::: ");
  await next();
  console.log(" ::: middlware end 2 ::: ");
});

app.get("/order", (c) => {
  console.log(" ::: order ::: ");
  return c.json({ message: "Hello word" });
});

만약 middleware와 route handler가 위와 같이 설정되어 있다면 실행 순서는 다음과 같다.

 ::: middlware start 1 ::: 
 ::: middlware start 2 ::: 
 ::: order ::: 
 ::: middlware end 2 ::: 
 ::: middlware end 1 :::

결과에서 볼 수 있듯이 request가 route handler에 도달하기 전까지는 먼저 선언된 middleware가 먼저 실행되며 request가 route handler에 의해 처리된 이후에는 response가 제일 마지막에 선언된 middleware에서 시작하여 제일 먼저 선언된 middleware 순서대로 이동하며 client에게 전달된다.

만약 middleware를 특정 route에만 적용하고 싶다면 다음과 같이 middleware를 추가할 때 route url도 함께 설정해준다. 아래 예제에서 logMiddleware는 /member route에만 적용된다.

...

app.use("/member", logMiddleware);

...

middleware의 logic을 재사용할 수 있도록 별도의 파일에 middleware logic을 구성한다면 다음과 같이 createMiddleware function을 통해 middleware를 만들어 적용할 수 있다.

middleware/logger.ts

import { createMiddleware } from "hono/factory";

export const logMiddleware = createMiddleware(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`);
  await next();
});

index.ts

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { logMiddleware } from "./middleware/logger.js";

const app = new Hono();

app.use(logMiddleware);

app.get("/order", (c) => {
  return c.json({ message: "Hello word" });
});

const port = 3000;
console.log(`Server is running on port ${port}`);

serve({
  fetch: app.fetch,
  port,
});

middleware에서 route handler로 들어가는 request object에 새로운 property를 추가하고 싶으면 다음과 같이 추가할 수 있다.

middleware/add-user.ts

import { createMiddleware } from "hono/factory";

type Middleware = {
  Variables: {
    user: { name: string };
  };
};

export const addUser = createMiddleware<Middleware>(async (context, next) => {
  context.set("user", { name: "test name" });
  await next();
});

index.ts

...
import { addUser } from "./middleware/add-user.js";

app.get("/order", addUser, (context) => {
  const user = context.var.user;
  console.log({ user });
  return context.json({ message: "Hello word" });
});

위의 예제와 같이 middleware에서 context의 set method를 통해 새로운 property를 설정하고 order router의 middleware로 추가하면 order route handler에서 context의 var property를 통해 middleware에서 추가한 property에 접근할 수 있다.

위의 예제를 통해 custom middleware를 생성하여 적용하는 방법을 살펴보았다. 이제 hono에서 built-in으로 제공하는 middleware중 일부를 살펴보자. Hono에서 제공하는 모든 built-in middleware list는 docuemntation에서 확인할 수 있다. ( Reference - middleware )

Bearer Authenticiation

Client request header에 포함된 Bearer Auth token을 검증할 때 사용할 수 있는 middleware. 해당 middleware가 적용된 router에 request를 보낼 때 Bearer 토큰 값Authoriazation header에 설정 해서 보내야 한다.

Middleware와 token 검사 logic은 다음과 같이 적용할 수 있다. verifyToken method의 return value가 true일 때 request가 route handler로 전달된다.

...

app.use(
  bearerAuth({
    verifyToken: async (token) => {
      // token 검사 logic
      return token ? true : false;
    },
  })
);

...

Body Limit

reqeust body의 max size를 설정할 때 사용할 수 있다. Request의 Content-Length header값을 먼저 확인하고 Content-Length header가 없으면 body의 size를 직접 체크하고 maxSize에 설정한 값보다 크면 middleware의 error handler를 실행한다.

import { bodyLimit } from "hono/body-limit";
...

app.post(
  '/profile-image',
  bodyLimit({
    maxSize: 1000 * 1024, // 1mb
    onError: (c) => {
      return c.json({ message: "body is too large" }, 413);
    },
  }),
  async (c) => {
    ...
    return c.json({ message: "upload success" });
  }
)

...

위의 예제에서 /profile-image route로 보낸 request의 body가 1mb이상이면 onError handler가 실행된다.

CORS

Cors middleware를 통해 cors 설정을 할 수 있다. 기본적인 설정 방법은 다음과 같다.

import { cors } from 'hono/cors'
...

app.use(
  cors({
    origin: 'http://example.com',
    allowHeaders: ['Authorization', 'Content-Type'],
    allowMethods: ['GET', 'OPTIONS', 'HEAD', 'POST', 'PATCH', 'PUT', 'DELETE'],
    credentials: true,
  })
)

...

만약 route에 따라 다른 cors policy를 적용해야 한다면 다음과 같이 복수로 적용할 수 있다.

app.use(
  'api/v1',
  cors({
    origin: 'https://example.com',
    allowHeaders: ['Authorization', 'Content-Type'],
    allowMethods: ['GET', 'OPTIONS', 'HEAD', 'POST', 'PATCH', 'PUT', 'DELETE'],
    credentials: true,
  })
)

app.use(
  'api/v2',
  cors({
    origin: ['http://example.com', 'https://example.com'],
    allowHeaders: ['Authorization', 'Content-Type'],
    allowMethods: ['GET', 'OPTIONS', 'HEAD', 'POST', 'PATCH', 'PUT', 'DELETE'],
    credentials: true,
  })
)

middleware에서 사용할 수 있는 option은 다음 documentation section을 통해 확인할 수 있다. ( Reference - CORS Middleware )

IP Restriction

IP restriction middleware를 통해 특정 ip에 대한 resource 접근을 허용 또는 차단할 수 있다.

import { ipRestriction } from 'hono/ip-restriction'
import { getConnInfo } from "@hono/node-server/conninfo";

...

app.use(
  ipRestriction(
    getConnInfo,
    {
      denyList: [],
      allowList: ["127.0.0.1", "::1"],
    },
    (remote, c) => {
      return c.json({ message: "Not Allowed" }, 403);
    }
  )
);

...

첫 번째 argument로 전달하는 getConnInfo는 hono를 실행하는 환경에 맞는 getConnInfo를 import하여 전달해야 한다. 테스트는 node 환경에서 진행하고 있으므로 node-server getConnInfo를 사용하며 만약 deno에서 hono를 실행한다면 다음과 같이 deno에서 export하는 getConnInfo를 사용해야 한다.

import { getConnInfo } from 'hono/deno'

...

allowList property에 router 접근을 허용할 ip list를 설정하고 denyList property에 접근을 막을 ip list를 설정할 수 있다. 만약 request 접근이 middleware에 의해 거부되면 세 번째 argument로 전달한 callback 함수가 실행된다.

Validation

Hono에선 request의 body나 header를 검사하기 위한 기본적인 validation middleware를 제공한다.

...
import { validator } from "hono/validator";

const app = new Hono();

app.post(
  "/order",
  validator("json", (value, c) => {
    if (!value.name) {
      return c.text("Invalid Input!", 400);
    }
    return {
      value,
    };
  }),
  async (c) => {
    ...
    return c.json({
      ok: true,
      message: "Hello world!",
    });
  }
);

위의 예제는 /order route에서 json type의 request body를 validator middleware에서 전달받아 validation을 작업을 하는 예제다. 만약 json type이 아닌 form type의 request body를 validation 해야 한다면 다음과 같이 json이 아닌 form을 첫 번째 argument로 전달해준다.

app.post(
  "/order",
  validator("form", (value, c) => {
    if (!value.name) {
      return c.text("Invalid Input!", 400);
    }
    ...
  }),
  ...
);

request body 뿐만아니라 header, query, param, cookie 역시 validator에서 검사할 수 있다.

app.post(
  "/order",
  validator("header", (value, c) => {
    if (!value.authorization) {
      return c.text("Unauthorized", 401);
    }
    ...
  }),
  ...
);

validator middleware에서 header를 validation할 때 value param을 통해 header key에 접근할 때 유의할 점은 아래와 같다.

// header from client
Authorization : Bearer ...

// validator에서 조회시
validator("header", (value, c) => {

  // .을 통해 조회할 때는 모두 소문자를 사용
  console.log(value.authorization);

  // client header와 같이 대문자를 포함하고 싶다면 다음과 같이 조회
  console.log(value['Authorization'])
  ...
}),

만약 body 뿐만 아니라 header validation도 함께 적용하고 싶다면 다음과 같이 여러개의 validator를 적용할 수 있다.

app.post(
  '/order',
  validator('header', ...),
  validator('json', ...),
  (c) => {
    ...
  }
)

이제 zod를 통해 validator middleware에서 validation 작업을 수행해보자. 우선 zod를 설치한다.

npm install zod

그리고 request body로 전달받아야 할 schema를 생성하고 validator에서 해당 schema를 기반으로 request body를 검사한다.

...
import { z } from "zod";

const schema = z.object({
  address: z.string(),
  city: z.string(),
});

app.post(
  "/order",
  validator("json", (value, c) => {
    const parsed = schema.safeParse(value);
    if (!parsed.success) {
      return c.text("Bad Request", 400);
    }
    return parsed.data;
  }),
  async (c) => {
    const body = await c.req.json();
    ...
    return c.json({
      ok: true,
      message: "Hello world!",
    });
  }
);

위의 예제에서 볼 수 있듯이 zod와 같은 validation library를 적용하면 보다 수월하게 validation 작업을 수행할 수 있다.

More from this blog

[ 살펴보기 ] TypeORM - Transactions, Migration

Transation Database 종류에 따라 detail한 부분은 차이점이 조금씩 있겠지만 각 sql statement는 개별적인 transaction block을 통해 실행되며 Database 설정에 따라 sql statement의 실행 결과가 자동으로 commit되어 영구히 적용되거나 commit을 직접 실행하기 전까지는 영구히 적용되지 않을 수 있다. 대부분의 경우 default로 sql statement 실행 결과가 자동으로 comm...

Feb 9, 20256 min read
[ 살펴보기 ] TypeORM - Transactions, Migration

Dev Diary

184 posts